WriteupBin
– A web challenge from hxp 36C3 CTF
https://ctftime.org/event/825
题目部署
本地搭建:
解压WriteupBin.tar.xz,在Dockerfile所在目录下执行:
echo 'hxp{FLAG}' > flag.txt && < /dev/urandom tr -dc a-f0-9 | head -c 16 > writeup-id.txt && docker build -t writeupbin . && docker run --cap-add=SYS_ADMIN --security-opt apparmor=unconfined -ti -p 8001:80 writeupbin
访问127.0.0.1:8001
这道题整个分析过程比题目本身更重要,所以我不会像普通的Writeup一样像个直通车每一步都走得特到位直抵flag,而是像走迷宫一样迂回式前进,每走一步都停下来分析,如果碰壁也要分析碰壁的原因。
题目分析
这道题基于这样一个发布和显示Writeup的平台。页面最上面可以浏览当前用户发布的WP;页面中间0f0e打头的这个字符串是当前session的用户id(并非PHPSESSID);下面的输入框可以写wp,点击submit提交后会跳转到show.php页面。
每一个wp都分配一个id,比如这里的73fd8aefbbc2768c
,这个id值和get参数id的值是对应的,都是相同的16位hex。show.php里面显示出了wp的内容,当前用户可以点赞,还可以把wp展示给Admin用户。
好像光看这些不知道从何下手。
我们手头还有源码包。
这个题目的源码压缩包应该是作为题目的附件直接提供给做题者的,所以先来瞅一眼压缩包里能给我们什么样的提示。
.
├── Dockerfile //Docker文件
├── admin.py //使用selenium模拟admin登录并点赞
├── db.sql //数据库文件
├── docker-stuff
│ ├── default //配置文件
│ └── www.conf //配置文件
├── www
│ ├── general.php //连接数据库设置header头等一些初始化操作
│ ├── html
│ │ ├── add.php //添加writeup相关操作
│ │ ├── admin.php //把writeup提交给admin
│ │ ├── index.php //入口文件
│ │ ├── like.php //点赞操作
│ │ ├── login_admin.php //admin登陆操作
│ │ └── show.php //获取writeup内容
│ └── views
│ ├── header.php //在页面上方展示目前id提交的writeup
│ ├── home.php //页面中部用来提供给用户输入的界面
│ └── show.php //点赞、提交给admin的展示页面
└── ynetd //用来启动 admin.py
有一堆php,还有一个.py文件,一个Dockerfile,一个.sql的数据库文件等等。
我们先来看看题目是怎么部署的,也就是看看Dockerfile文件里有什么名堂。
COPY db.sql writeup-id.txt flag.txt /root/
可以看到flag文件是先从源码的根目录复制到了docker里的root目录下,
RUN replace '__FLAG__' "$(cat /root/flag.txt)" -- /root/db.sql
然后flag.txt里面的内容又被写到了root目录下db.sql这个数据库文件里,flag的真实值替换掉了数据库文件里flag的占位符__FLAG__
。
一同被写入db.sql的还有writeup_ID、数据库密码等等。
replace '__DB_PASSWORD__' "$(< /dev/urandom tr -dc A-Za-z0-9 | head -c32)" -- /root/db.sql /var/www/general.php && \
replace '__WRITEUP_ID__' "$(cat /root/writeup-id.txt)" -- /root/db.sql /var/www/html/admin.php && \
< /dev/urandom tr -dc A-Za-z0-9 | head -c32 > /root/admin-token.txt && \
replace '__ADMIN_TOKEN__' "$(cat /root/admin-token.txt)" -- /home/ctf/admin.py && \
replace '__ADMIN_HASH__' "$(php -r 'echo password_hash($argv[1], PASSWORD_DEFAULT);' -- $(cat /root/admin-token.txt))" -- /var/www/html/login_admin.php
再来看db.sql是如何处理这些写入的数据的:
值得关注的语句如下:
db.sql
USE `writeupbin`;
INSERT INTO `writeup` (id, user_id, content) VALUES ('__WRITEUP_ID__','admin','__FLAG__');
相当于Writeup_ID的值、“admin”、还有flag的值分别插入到了writeupbin数据库下writeup表中id、user_id、content这三个数据项下。
id | user_id | content |
---|---|---|
__WRITEUP_ID__的值 | admin | __FLAG__的值 |
顺着这个思维继续往前走,数据库里面的记录是如何被网页调用的呢?
我们来到 /var/www/html/show.php
$stmt = $db->prepare('SELECT id, content FROM `writeup` WHERE `id` = ?');
$stmt->bind_param('s', $_GET['id']); //防止SQL注入
$stmt->execute();
$writeup = mysqli_fetch_all($stmt->get_result(), MYSQLI_ASSOC)[0];
可以看到,show.php通过get请求参数‘id’获取到id号(这个id就是前面提到的每个wp的编号),然后把id的16位hex值代入sql查询语句,将writeup表的相关数据取出来存到$writeup变量里。
show.php底部包含了 …/views/show.php 这个文件
include('../views/show.php');
而$writeup变量就是在这里被调用的
<?= $writeup['content'] ?>
,在/views/show.php页面里将id对应的content显示出来。
这下就明了了:拿flag的方法,就是输入admin的Writeup ID(唯一)作为show.php的get参数提交,这样从数据库取出的content就是flag的值,会在show.php页面里显示出来。可以这么理解:admin用户唯一的那个writeup的内容就是flag值。
但是怎么获取到admin的writeup id呢?
先说句题外话:对于数据库writeup表中非admin用户的记录,id和content两个字段存放的其实就是我们在index界面输入框提交的wp的编号和内容,user_id存放的是session id。
id | user_id | content |
---|---|---|
writeup的id | $_SESSION[‘id’] | writeup的内容 |
这个从add.php里可以体现出来:
$stmt = $db->prepare('INSERT INTO `writeup` (id, user_id, content) VALUES (?,?,?)');
$id = id();
$stmt->bind_param('sss', $id, $_SESSION['id'], $_POST['content']);
$stmt->execute();
总结一下:
Writeup数据表
写入数据库方式 | 用户 | id(数据项) | user_id(数据项) | content(数据项) |
---|---|---|---|---|
docker部署时写入 | admin用户 | __WRITEUP_ID__的值(我们的 目标) | admin | FLAG |
网页输入框提交 | 非admin 用户1(session1) | Writeup 1-1的id (16位hex) | $_SESSION[‘id’] Session 1 用户id (16位hex) | Writeup 1-1的内容 |
网页输入框提交 | 非admin用户1(session1) | Writeup 1-2的id (16位hex) | $_SESSION[‘id’] Session 1 用户id (16位hex) | Writeup 1-2的内容 |
… | 非admin用户1(session1) | Writeup 1-n的id (16位hex) | … | Writeup 1-n的内容 |
网页输入框提交 | 非admin用户2(session2) | Writeup 2-1的id (16位hex) | $_SESSION[‘id’] Session 2 用户id (16位hex) | Writeup 2-1的内容 |
… | … | … | … | … |
网页输入框提交 | 非admin用户n(session n) | Writeup n-1的id (16位hex) | $_SESSION[‘id’] Session n 用户id (16位hex) | Writeup n-1的内容 |
我们把目光重新聚焦到如何获取admin的id上来。
很容易想到的一个想法就是,index页面上会显示出当前session用户所撰写的所有wp的id,点进去就是一个个wp,如果我们把当前session的用户id改成admin,那么岂不是就能显示出admin的writeup id了吗?
这种可能性应该是没有的,要不然这个题目就太简单了。。。
保险起见还是分析一下。
我们看一下general.php,Session id就是在这里生成的。
function id() {
return bin2hex(random_bytes(8));
}
...
if( ! isset($_SESSION['id'])) {
$_SESSION =</