首页
社区
课程
招聘
[原创]一串反引号,十层突破:n1ctf-2018-easy_harder_php 从备份泄露到 MySQL 提权读 flag
发表于: 2天前 426

[原创]一串反引号,十层突破:n1ctf-2018-easy_harder_php 从备份泄露到 MySQL 提权读 flag

2天前
426

目标:看雪 KCTF n1ctf-2018-easy_harder_php(Nu1L),PHP 5.5.9 + Apache 2.4.7 + MySQL,动态 Docker 靶机
结果:flag{9afddef5-5650-4032-9011-591e203e8223}
阅读本文你将获得:一条从 ~ 备份文件 → SQL 注入盲注 → SoapClient 反序列化 SSRF → 短标签图片马上传 → LFI 包含 RCE → 启动脚本溯源 flag 的完整链路,以及我在每层踩过的坑与排错方法。

平台给了一个 :81 端口的 PHP 站点,入口是登录页。习惯性试了试常见备份文件,没想到直接命中:

Apache 对 ~ 备份文件不做任何处理,直接当静态文件吐出。三个文件把整套业务逻辑摊开——这个题的灵魂全在 user.phpDb::get_column()

注入根因get_column 给列名套上反引号,而 insert() 里又用 preg_replace('/([^,]+)/',''{1}\'',this->get_column(values))把反引号替换成单引号——替换发生在SQL拼接之前。所以任何进入values的、含反引号的字符串,都会被"洗"成单引号注入点。而publish()里 signature 字段原样进$values`:

signature 只被 addslashes 处理(addsla_all()),引号会被转义,但反引号原样保留。反引号被 preg_replace 洗成单引号 → 单引号被 addslashes 转义过 → 但替换发生在 addslashes 之后……于是单引号在替换时"复活"了。这就是整个题的地基。

注入点在 INSERT INTO ctf_user_signature(...) VALUES ('...','...','SIGNATURE','...')。把 signature 构造成:

洗完之后变成:

即一次插入三行:第 1 行是我自己的正常签名(保证不破坏业务),第 2 行的第 3 列放裸 SQL 子查询(被反引号对保护,不参与 preg_replace 的替换),# 注释掉后面的语句。

踩坑 1:第 1 行的 mood 列必须塞合法的 Mood 序列化。 一开始我写 `x`,`y`)#,把 y 写进 mood 列,showmess()unserialize('y') 直接失败,index 页 500,整个注入通道被污染,排查了半天。正确做法是第 1 行 mood 给一个真的 Mood 对象序列化:

踩坑 2:第 2 行的 userid 必须等于我自己的 userid。 showmess() 查询 WHERE userid = $this->userid,一开始硬编码 2,平台 userid 是共享自增的,永远不命中,SoapClient 从来没被触发。用布尔盲注拿自己的 userid 后再拼:

布尔盲注通道:子查询 (select if(COND,1,(select 1 union select 2))) —— 真时是合法单值(INSERT 成功,返回 OK),假时是 2 行子查询(INSERT 报错,返回 ERR)。这个通道非常稳定,全靠它把 admin 的密码 md5 逐字符挖出来:

密码有了,但登录时:

源码说得很清楚:

admin 的 ip = 127.0.0.1allow_diff_ip = 0,而 get_ip() 就是 $_SERVER['REMOTE_ADDR']只有 REMOTE_ADDR 是 127.0.0.1 的请求才能登录 admin。直接伪造 X-Forwarded-For: 127.0.0.1 无效(Apache 没配 mod_remoteip,REMOTE_ADDR 只看 TCP 源地址)。

要"从 127.0.0.1 发请求",只能靠 SSRF。而 showmess() 恰好有一个反序列化触发点:

如果 mood 列反序列化出来的是 SoapClient 对象,getcountry() 是它的不存在方法,会走 __calldo_soap_call__doRequest,向 SoapClient 的 location 发起一次真实的 HTTP 请求——这就是 SSRF

PHP 5.5 的 SoapClient 序列化属性是 public 的,且 _user_agent 带下划线(11 字符)。最小触发构造:

make_http_soap_request 生成请求头时会原样使用 _user_agent 的值,所以在这里注入 \r\n 就能伪造整段 HTTP 头 + 提前闭合头部 + 塞入自定义 body:

踩坑 3:password 必须传明文。 login()$password = md5($_POST['password']),我第一次把 md5 值塞进 body,结果服务器对 md5 值再算一次 md5,永远匹配不上——卡了很久才发现这个"二次哈希"陷阱。

踩坑 4:验证码 code 的竞态。 views/login 每次 GET 都会刷新 $_SESSION['code'],而 code 是 substr(md5(?),0,5) 的 4 位爆破。一开始用 LFI 读 session 文件拿 code 再 POST,十次有八次报 code erroar。最后发现最稳的方式:用一个"未登录的新 session",GET login 拿 hint → 爆破 code → 立即 POST(同一个 code 在同一秒内不会失效)。因为目标 session 全程没有别的请求去刷新它,SoapClient 的伪造 POST 到达时 code 依然匹配。

踩坑 5(本靶机最关键的坑):location 的端口。 旧靶机上 location 用 212K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5J5y4#2)9J5k6e0m8Q4x3X3f1H3i4K6u0W2x3g2)9K6b7e0R3I4i4K6u0r3i4K6u0W2i4K6u0W2i4K6u0W2 死活不通(TCP 连接建立但请求没到应用,Apache 81 端口没有 vhost)。平台回收换新靶机后,把 location 改成 8beK9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8U0p5J5y4#2)9J5k6e0m8Q4x3X3f1H3i4K6u0W2x3g2)9J5c8X3W2F1k6r3g2^5i4K6u0W2M7r3S2H3i4K6y4r3j5h3y4@1K9h3!0F1i4K6y4p5L8r3!0Y4K9h3^5`.(80 端口,应用 vhost 所在),一次就通了。

触发后读目标 session:

admin 登录成功。 至此 SSRF 反序列化链闭环。

admin 的 publish() 走上传分支。上传逻辑:

两个关键点:

绕过:用 <?= 短标签。<?= 是 PHP 5.4+ 永远可用的输出标签,不含 <?php 子串,stripos 查不到,文件不会被删。图片马:

index.phprequire_once 'views/'.$_GET['action'] 是经典 LFI。包含上传的图片马:

<?= 被执行,命令输出回显在页面里。爆破脚本几分钟内命中 sh178624023488.jpg,RCE 确认:

踩坑 6:payload 别带单引号嵌套。 第一次把 find / -name '*flag*' 塞进 shell_exec('...'),单引号把 PHP 字符串炸了,静默失败。命令内要用双引号,外层 PHP 用单引号;或者外层用双引号(注意 $ 变量解析)。

RCE 之后开始找 flag:

环境变量里的 FLAG 值居然是 not_flag——一个明摆着的假 flag。正当我以为这题是"拿不到 flag 的摆烂题"时,ls -la / 发现了根目录的 /run.sh(容器启动脚本)

真相大白:平台启动时把真实 $FLAG 先写进 MySQL 的 flag,然后才把环境变量覆盖成 not_flag 防泄露。所以真实 flag 在数据库里,而且:

最后一步,用 root 连库:

FLAG 到手。

这道题最妙的地方在于:每一个"防护"都在无意间指向下一条路。反引号转单引号的 bug 给了 SQL 注入;admin 的 127.0.0.1 限制逼出了 SoapClient SSRF;clean_danger.sh 只查 <?php 让短标签钻了空子;环境变量 FLAG=not_flag 反而把注意力引向启动脚本,最终在 MySQL 里挖出真身。

十层突破,环环相扣——这就是 n1ctf-2018-easy_harder_php。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回