FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。
FreeBuf+小程序把安全装进口袋
在这个目录下执行ubi_reader0.ubi,但是我本身拥有这个ubi_reader,是从binwalk源码下载的,他自己却不能一次binwalk解压成功,确实让人匪夷所思,可能是要多加一些什么参数吧
首先在/etc/nginx/conf.d/目录下有很多nginx的配置模块
首先看/etc/nginx/conf.d/rest.url.conf
看到它分为五个模块,其中主要看第三个模块,即文件上传模块
在Nginx的官方文档中定义的location的语法结构为:
location[=|~|~*|^~]uri{...}其中uri就是我们的/api....那一串,它也可以含正则表达
因此就是对Authorization这个环境变量进行判定,猜测是身份证明,如果没有的话就会403报错
upload_pass/form-file-upload;upload_store/tmp/upload;upload_store_accessuser:rwgroup:rwall:rw;upload_set_form_field$upload_field_name.name"$upload_file_name";upload_set_form_field$upload_field_name.content_type"$upload_content_type";upload_set_form_field$upload_field_name.path"$upload_tmp_path";upload_aggregate_form_field"$upload_field_name.md5""$upload_file_md5";upload_aggregate_form_field"$upload_field_name.size""$upload_file_size";upload_pass_form_field"^.*$";upload_cleanup400404499500-505;upload_resumableon;之后便是对nginxuploadmodule配置参数了
upload_pass/form-file-upload:指明了后续要处理的php文件,转到后端处理/form-file-upload这个参数
upload_store/tmp/upload:指定了上传文件的存放地址
upload_store_accessuser:rwgroup:rwall:rw:上传文件的访问权限,所有人都是rw
upload_set_form_field$upload_field_name.name"$upload_file_name":设置参数
upload_pass_form_field"^.*$":从表单原样转到后端参数,是一个正则匹配^表示开始,.表示任意字符,*表示可以出现零次或多次,$是结束,因此说明可以上传任意字符到后端
upload_cleanup400404499500-505:如果出现了400404等这些错误就删除所有上传的文件
在这里没有对cookie_sessionid进行判断
cookie_sessionid
Authorization
可以看到正规的/upload是存在对cookie_sessionid的判定的,($cookie_sessionid~*"^[a-f0-9]{64}")正则匹配要确保它是一个有效的64位十六进制字符串
之后看/form-file-upload到底是什么
其中包含了uwsgi_params这个配置文件,在/etc/nginx/uwsgi_params,是一种用于配置与Nginx之间的通信的文件。它定义了一些变量和选项,以确保正确地传递请求和响应。算是一种通用配置
说明了它把请求发送给了uwsgi的服务器来处理
那么处理的文件是什么,我们先看nginx的启动程序
在最后启动了uwsgi程序
可以看到uwsgi的启动程序指定为/usr/bin/uwsgi-launcher程序
它执行了三个初始化程序,分别看一下
[uwsgi]plugins=cgiworkers=4master=1uid=www-datagid=www-datasocket=127.0.0.1:9000buffer-size=4096cgi=/jsonrpc=/www/cgi-bin/jsonrpc.cgicgi-allowed-ext=.cgicgi-allowed-ext=.plcgi-timeout=3600ignore-sigpipe=true首先是启用了CGi插件来处理CGI脚本,配置了四个工作进程来处理请求,启用了主进程管理工作进程,设置身份,监听9000端口,设置缓冲区大小为4096,将/jsonrpc路径映射到/www/cgi-bin/jsonrpc.cgi脚本,允许运行的为.cgi.pl的扩展名,忽略了signal信号。总之就是监听9000端口,运行cgi脚本。接下来的两个ini也是如此,就不过多赘述了,但是看到我们之前分析的/upload的uswgi是发送到9003端口来处理请求的,因此看到
发现是/www/cgi-bin/upload-cgi来处理请求的,调用的方式是先fork了一个子进程,然后execvp来执行upload.cgi
在分析upload-cgi前要先看一些自定义函数的定义,不然会较难分析
_DWORD*StrBufCreate(){_DWORD*v0;//r4_BYTE*v1;//r0v0=malloc(0xCu);if(v0){v0[1]=0x37;v1=malloc(0x37u);*v0=v1;if(!v1){free(v0);return0;}*v1=0;v0[2]=0;}returnv0;}StrBufCreate函数,没有参数,作用是创建一个三个地址长度的chunk,然后第二个长度写Buf的长度即0x37,低三个长度是0,第一个长度是Buf的地址,还算是比较清晰的
如果感兴趣可以自行分析
int__fastcallStrBufSetStr(inta1,inta2){StrBufClear(a1);returnStrBufAppendStr((_DWORD*)a1,(constchar*)a2);}便是清除a1,然后a2覆盖在a1里面,即可以理解为赋值
_DWORD*__fastcallStrBufToStr(_DWORD*a1){_DWORD*result;//r0result=StrBufIsOK(a1);if(result)return(_DWORD*)*a1;returnresult;}判断是否合法,如果合法则返回地址,不合法就返回错误信息
intjsonutil_get_string(inta1,_DWORD*a2,...){va_listvarg_r2;//[sp+10h][bp-8h]BYREFva_start(varg_r2,a2);if(!jsonutil_vget(a1,(int*)varg_r2))return-1;*a2=json_object_get_string();return0;}整体来看,这段代码的目的是从一个JSON对象中提取字符串。jsonutil_get_string函数负责调用jsonutil_vget进行处理,而jsonutil_vget则通过不断检查和获取JSON对象的值,最终返回一个有效的索引或对象。若成功获取字符串,则将其存储在传入的指针*a2中。
char*__fastcallmultipart_parser_init(constchar*a1,inta2){size_tv4;//r0char*v5;//r4size_tv6;//r0char*result;//r0v4=strlen(a1);v5=(char*)malloc(2*v4+37);strcpy(v5+24,a1);v6=strlen(a1);*((_DWORD*)v5+2)=v6;*((_DWORD*)v5+1)=0;*((_DWORD*)v5+5)=&v5[v6+25];result=v5;v5[12]=2;*((_DWORD*)v5+4)=a2;returnresult;}这段代码的功能是为一个多部分解析器分配内存并初始化其状态,包括存储输入字符串及其长度、设置一些状态字段等
multipart_parser_execute(v12,v8,v13);该函数源码过长就不贴了主要的作用可能是用于解析HTTP请求中的multipart/form-data,但是由于可控段不在这里就不过多的分析了,详细请看下文
分析到这里,我认为这些函数,想必是网上开源的函数,大概率是没有什么漏洞可言的,因此主要看函数的作用以及主函数的逻辑漏洞
int__fastcallsub_115EC(constchar*a1,constchar*a2,constchar*a3){boolv3;//zfconstchar*v8;//r4intv9;//r4chars[316];//[sp+Ch][bp-13Ch]BYREFv3=a3==0;if(a3)v3=a1==0;if(v3)return-1;if(!strcmp(a1,"Firmware")){v8="/tmp/firmware/";}elseif(!strcmp(a1,"Configuration")){v8="/tmp/configuration/";}elseif(!strcmp(a1,"Certificate")){v8="/tmp/in_certs/";}elseif(!strcmp(a1,"Signature")){v8="/tmp/signature/";}elseif(!strcmp(a1,"3g-4g-driver")){v8="/tmp/3g-4g-driver/";}elseif(!strcmp(a1,"Language-pack")){v8="/tmp/language-pack/";}elseif(!strcmp(a1,"User")){v8="/tmp/user/";}else{if(strcmp(a1,"Portal"))return-1;v8="/tmp/www/";}if(!is_file_exist(a2))return-2;if(strlen(a2)>0x80||strlen(a3)>0x80)return-3;if(match_regex("^[a-zA-Z0-9_.-]*$"))return-4;sprintf(s,"mv-f%s%s/%s",a2,v8,a3);debug("cmd=%s",s);if(!s[0])return-1;v9=system(s);if(v9<0)error((int)"upload.cgi:%s(%d)Uploadfailed!",(int)"prepare_file",(constchar*)0xAD);returnv9;}会发现到最后会执行mv-fa2v8/a3
v8是通过判断a1来获得的,a2和a3分别都是参数
a2比较显然是通过file.path表单名称来匹配的
a3会稍微复杂一点,filename表单的参数如果存在且不包含.img的话,那么a3就是filename表单下的参数,如果filename表单下为空的话,那么fileparam表单下的参数就是a3了,显然第二种方法控制比较简单
但是呢,如果只是到了这里,那么该漏洞也不会有任何的作用,可是呢
但是还有一个问题
此时该判定该如何跳过,接下来的动态调试也会解释这个问题
这一段过于的抽象,因此可以通过调试来观看整个进程
在程序的开始有个自己跳自己,那么该子进程就会一直在这里执行,同时在调试出错也可以重复进行,可以说是很方便的方法。
然后使用gdbserver配合gdb进行调试,看到此时有个子进程,执行gdbserver附加进程
修改完成之后便是可以看到程序正常进行,同时还是从开头执行的
所以这里的函数就是读取表单字段的内容存储在变量里
看到上述的执行流走偏的情况,发现他正则匹配的对象是file.path的内容
返回值为0,所以绕过了检查,但是这个函数是什么呢,很好奇
发现只要是0-9内的10位数都可以绕过检查,也是很不错的
但是呢
if(!is_file_exist(a2))return-2;要求是个存在的文件,因此在fork的时候看一眼
所以只能是0000000001了,还是要反复匹配才是正确的
配置网络环境,运行以下脚本
pulseaudio是音频设备,想来你也不需要用qemu机看电影,那么应该是没有什么影响的,把rootfs压缩好scp传上来
之后在机子里运行老几样建立隔离环境
chmod-R777rootfscdrootfs/mount--bind/procprocmount--bind/devdevchroot./bin/sh之后执行
到最后会执行mv-f/tmp/upload/0000000001/tmp/www//login.html便是复现成功了
winmt师傅还有一种Poc可以更加的优雅来控制file.path,其实报文会根据配置文件来自动来添加一个xxx.path
因此会把原始字段的名称先赋成上传文件的名称,然后上传文件的名称.path就会记录在上传的位置,即/tmp/upload/0000000001,因此可以用更优雅的方式进行利用
------------Content-Disposition:form-data;name="pathparam"Portal------------Content-Disposition:form-data;name="fileparam"login.html------------Content-Disposition:form-data;name="file";filename="login.html";Content-Type:application/octet-stream