hxp 36C3 CTF Web题 WriteupBin Writeup (Selenium模拟点击+Content Security Policy+Nonce+Parsley.js触发错误提示)
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 = ['id' => id(), 'c' => id()];
}
可以看到session id是后端随机生成的,好像不可控。
但是浏览代码,发现在login_admin.php中有为$_SESSION[‘id’]赋值的操作。
if (!isset($_SERVER['PHP_AUTH_USER'])) {header('WWW-Authenticate: Basic realm="admin.py"');header('HTTP/1.0 401 Unauthorized');exit();
}
if ($_SERVER['PHP_AUTH_USER'] === 'admin' && password_verify($_SERVER['PHP_AUTH_PW'], '__ADMIN_HASH__')) {$_SESSION['id'] = 'admin';redirect('/show.php?id='. $_GET['id']);
}
首先这里可以看到有个Basic认证,提示信息为admin.py,正好源码里面有个文件也叫这个名字。admin.py有什么用后面会讲解。
$_SESSION[‘id’]变为admin的前提有二:
- Basic认证输入的用户名(即$_SERVER[‘PHP_AUTH_USER’]) 为 ‘admin’
password_verify($_SERVER['PHP_AUTH_PW'], '__ADMIN_HASH__')
为真,即Basic认证输入的密码为__ADMIN_HASH__
而Docker在部署时无论密码原文__ADMIN_TOKEN__还是密码哈希__ADMIN_HASH__的值就已经被确定了,我们无从得知。所以我们想凭一己之力让$_SESSION[‘id’]变为admin是不可能的。
总结下来就是,session id不可控。
那么我们作为普通用户还能够怎么操作从而跟admin用户建立起联系呢?
想到在Writeup详情页面(也就是show.php?id=<writeup id>)中有一个Show to admin的按钮,按钮上方Liked by显示的是用户给这篇writeup的点赞情况。
点击Show to admin后两三秒钟内回到刚刚的页面可以看到点赞情况还没有发生变化,但是再过一小会儿刷新页面就可以看到Liked by名单里就已经出现了admin的名字。
所以我们有两点收获:
- 透过Show to admin按钮和admin的反馈,我们可能能够建立起普通用户与admin之间的联系,这极有可能是突破口。
- admin用户的点赞并非Show to Admin按钮触发后的即时反应,存在延时,这里面可能有一个触发机制。
我们回到源码,来看Show to Admin和admin点赞背后的逻辑。
/views/show.php 表单提交
<form method="post" action="/admin.php"><input type="hidden" name="c" value="<?= $_SESSION['c'] ?>"><input type="hidden" name="id" value="<?= $writeup['id'] ?>"><input type="submit" value="Show to Admin">
</form>
c和id两个参数被POST提交,其中c参数的值为$_SESSION[‘c’] 的值,而$_SESSION[‘c’] 与$_SESSION[‘id’]一样都是随机生成的16位hex;id参数的值就是Writeup ID。两参数提交后跳转到admin.php里,
admin.php
$fp = stream_socket_client('tcp://127.0.0.1:1024', $errno, $errstr, 2);
if (!$fp) {die('admin seems to be too busy');
} else {fwrite($fp, $_POST['id']."\n");fclose($fp);
}
而admin.php通过tcp连接,把 $_POST[‘id’] 也就是Writeup ID写到数据流中去。
数据流流向何处呢?Dockerfile里面有这样一句:
CMD ynetd -lm -1 -lt 5 -t 60 -lpid 256 -sh n /home/ctf/admin.py
执行了ynetd这个ELF文件,-lm -1 -lt 5 -t 60 -lpid 256 -sh n /home/ctf/admin.py
都是ynetd的参数,最后一个参数是admin.py。此处这个ynetd的功能就是启动admin.py并将tcp流作为py文件的标准输入流。连贯起来就是,admin.php通过数据流把writeup id传给了admin.py。
再来看admin.py。
首先肯定要接收点击Show to Admin后输入流传过来的writeup id。
writeup_id = input().strip()
总的来说,Admin.py的功能就是通过selenium来模拟admin用户点赞的操作。
Selenium 是一个用于Web应用程序的自动化测试工具,Selenium会生成一个浏览器的环境,模拟浏览器的行为,就像真正的用户在操作一样。很多人用它来写爬虫。
admin.py
display = Display(visible=0, size=(800, 600))
display.start()
chrome_options = Options()
chrome_options.add_argument('--disable-gpu')
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
driver = webdriver.Chrome('/usr/bin/chromedriver', options=chrome_options)url = 'http://admin:__ADMIN_TOKEN__@127.0.0.1/login_admin.php?id='+writeup_id
driver.get(url)
element = driver.find_element_by_xpath('//input[@id="like"]')
element.click()driver.quit()
display.stop()
很容易理解,先建立一个显示窗口,加入一些chrome访问的选项,然后驱动一个chrome浏览器去以admin身份访问含特定Writeup ID的login_admin.php。
然后通过find_element_by_xpath这个函数,找到id为like的第一个input标签,即点赞按钮的标签,然后模拟点击点赞按钮。
/views/show.php
<form method="post" action="/like.php"><input type="hidden" name="c" value="<?= $_SESSION['c'] ?>"><input type="hidden" name="id" value="<?= $writeup['id'] ?>"><input id="like" type="submit" value="
hxp 36C3 CTF Web题 WriteupBin Writeup (Selenium模拟点击+Content Security Policy+Nonce+Parsley.js触发错误提示)相关推荐
- ctf的web题目php,32C3 CTF 两个Web题目的Writeup
0x00 简介 作为一个销售狗,还能做得动Web题,十分开心. 这次搞了两个题目,一个是TinyHosting,一个是Kummerkasten. 0x01 TingHosting A new file ...
- CTF Web题 部分WP
1.web2 听说聪明的人都能找到答案 http://123.206.87.240:8002/web2/ CTRL + u 查看源代码 2.计算器 http://123.206.87.240:8002 ...
- CTF平台题库writeup(一)--南邮CTF-WEB(部分)
WEB题 1.签到题 题目:key在哪里? writeup:查看源代码即可获得flag! 2.md5 collision 题目: <?php $md51 = md5('QNKCDZO'); $a ...
- i春秋python_i春秋CTF web题(1)
之前边看writeup,边做实验吧的web题,多多少少有些收获.但是知识点都已记不清.所以这次借助i春秋这个平台边做题,就当记笔记一样写写writeup(其实都大部分还是借鉴其他人的writeup). ...
- CTF web题 wp:
1.签到题 火狐F12查看源码,发现注释: 一次base64解码出flag 2.Encode 在这里插入图片描述 和第一题界面一样?? 轻车熟路f12: 发现编码: 格式看上去是base64,连续两次 ...
- python selenium模拟点击
文章目录 1.安装谷歌浏览器 2.安装浏览器驱动 3.安装selenium 4.简单测试 5.打卡程序 6.linux设置定时任务 7.其他 1.安装谷歌浏览器 #下载包 wget https://d ...
- 【Python】Selenium模拟点击网页下载文件
整个流程大致如下: 1.首先需要在http://chromedriver.storage.googleapis.com/index.html中下载chrome浏览器版本对应的驱动文件,可以在浏览器[设 ...
- BSides Noida CTF 2021 web题wowooofreepoint writeup(两道反序列化)
emmm终于开始正经地写第一篇wp了!撒花撒花~这场比赛也算是我第一个没爆零的比赛,自己独立做出来了一道(半?),拿到flag也是相当开心~(当然还是比较菜,大佬轻喷)比完赛之后自己又去把差一点做出来 ...
- mysql 南邮ctf_南邮ctf web题记录(上)
1.签到题 f12看源码就行了. md5 collision 题目贴出了源码,按照题意,a不等于QNKCDZO但是md5与QNKCDZO的md5相等时就可以获得flag. 如果两个字符经MD5加密后的 ...
最新文章
- Mysql 源码安装
- Io 异常: The Network Adapter could not establish the connection解决方案
- 网格自适应_Abaqus网格重划自适应技术
- 如何删除子域信任关系?
- jeesite缓存问题
- 浅析C#中构建多线程应用程序
- K8S 的报错问题解决
- 使用原生Java代码生成可执行Jar包
- Elasticsearch写入webshell漏洞(WooYun-2015-110216)
- 计算机cpu电压,怎么样调电脑cpu电压啊
- 保护系统 WinXP故障恢复控制台完全指引
- 写给30岁的自己,以及所有即将、正在、已经奔三的朋友们
- 冒志鸿:没有对比就没有伤害,原来中国的区块链这么……
- slides.com 导出PDF
- 移动端高清、多屏幕适配方案
- 易语言可以编译c语言,刷屏软件?其实易语言也可以做这种软件
- 解决Ubuntu 20.04 播放视频,因缺少编解码器无法处理音频/视频流,以及解决‘因没有公钥,无法验证下列签名’问题
- 华为云服务器(Centos7)安装与卸载mysql8
- 到微软下载VS2008
- Linux 性能监控工具命令大全
热门文章