垃圾桶

EL PSY CONGROO.

Lz1y's avatar Lz1y

CISCN 2019 pmarkdown writeup

CISCN_2019_final_pmarkdown

题目源码: https://github.com/imagemlt/CISCN_2019_final_pmarkdown

线上靶机: http://web65.buuoj.cn/

前几天打了国赛,发现自己真的菜的不行,web被虐得心态有点问题了,第一天的web就只看了这一道题,最后还差一点,被卡死了,现在复盘一下。

当时是直接把web源码以及php扩展so文件给选手了,看题目源码的意思,很明显就是需要SSRF上传文件。

但是PHP代码中完全没有ssrf点,所以问题很明显就是需要去逆向扩展文件了,由于现场不能直接使用网络,所以第一次逆扩展的经历十分蛋疼,很多结构体以及内置函数需要反反复复的去查,很麻烦。

不说废话了,开始分析。
PHP中,使用到的扩展函数只有一个pmark_include

通过导出函数直接定位到函数地址

无明显功能,调用了verbose_pandoc_file

int __fastcall verbose_pandoc_file(char *filename)
{
  return verbose_pandoc_file(filename);
}


最后发现调用了外部命令,使用pandoc,将md文件渲染。此处也并没有明显漏洞,没有命令注入之类的问题,那么这条路目前就断了,我判断利用点也并不是此处。

如果了解过PHP扩展,那么很容易想到RINIT函数。
https://www.lz1y.cn/2018/10/29/PHP-extension-rootkit/
之前的文章有介绍过PHP的生命周期,随便看一眼就会懂了。
如何声明一个扩展的请求初始化函数呢?那就需要使用zend_module_entry结构体了。

在IDA中看扩展的导出表,同样能很快找到这个结构体

找到了请求初始化函数

// local variable allocation has failed, the output may be wrong!
int __fastcall zm_activate_pmarkdown(int type, int module_number)
{
  char *v2; // r13
  __int64 v3; // rbp
  __int64 v4; // rbx
  char *v5; // rdi
  _QWORD *v6; // rbp
  char *v7; // rsi
  _QWORD *v8; // r13
  const char *v9; // rbx
  int v10; // eax
  bool v11; // cf
  bool v12; // zf
  signed __int64 v13; // rcx
  __int64 v14; // rbx
  __int64 v15; // rax
  _BYTE *v16; // rax
  _QWORD *v18; // rbx
  char v19; // dl
  char suffix[10]; // [rsp+Eh] [rbp-3Ah]
  unsigned __int64 v21; // [rsp+18h] [rbp-30h]

  v21 = __readfsqword(0x28u);
  if ( *((_BYTE *)&core_globals + 466) )
  {
    v18 = (_QWORD *)_emalloc_32(*(_QWORD *)&type, *(_QWORD *)&module_number);
    *v18 = 25769803777LL;
    v18[1] = 0LL;
    v18[2] = 7LL;
    *((_DWORD *)v18 + 6) = 1380275039;
    *(_QWORD *)&type = v18;
    *((_WORD *)v18 + 14) = 17750;
    *((_BYTE *)v18 + 30) = 82;
    *((_BYTE *)v18 + 31) = 0;
    zend_is_auto_global(v18);
    v19 = *((_BYTE *)v18 + 5);
    if ( !(v19 & 2) )
    {
      v12 = (*(_DWORD *)v18)-- == 1;
      if ( v12 )
      {
        *(_QWORD *)&type = v18;
        if ( v19 & 1 )
          free(v18);
        else
          _efree(v18);
      }
    }
  }
  v2 = (char *)*((_QWORD *)&core_globals + 52);
  v3 = _emalloc_40(*(_QWORD *)&type, *(_QWORD *)&module_number);
  *(_QWORD *)v3 = 25769803777LL;
  *(_QWORD *)(v3 + 8) = 0LL;
  *(_QWORD *)(v3 + 16) = 8LL;
  *(_BYTE *)(v3 + 32) = 0;
  *(_QWORD *)(v3 + 24) = 5065499905268664400LL;
  v4 = _emalloc_40(*(_QWORD *)&type, *(_QWORD *)&module_number);
  *(_QWORD *)(v4 + 24) = 6074869145814650692LL;
  *(_QWORD *)v4 = 25769803777LL;
  *(_QWORD *)(v4 + 8) = 0LL;
  *(_QWORD *)(v4 + 16) = 13LL;
  *(_DWORD *)(v4 + 32) = 1330598495;
  *(_BYTE *)(v4 + 36) = 84;
  *(_BYTE *)(v4 + 37) = 0;
  v5 = v2;
  v6 = (_QWORD *)zend_hash_find(v2, v3);
  v7 = (char *)v4;
  v8 = (_QWORD *)zend_hash_find(v2, v4);
  if ( v6 )
  {
    v9 = (const char *)(*v6 + 24LL);
    v10 = strlen(v9);
    v5 = ".md";
    v7 = strncpy(suffix, &v9[v10 - 3], 3uLL);
    v13 = 3LL;
    do
    {
      if ( !v13 )
        break;
      v11 = (unsigned __int8)*v7 < (unsigned __int8)*v5;
      v12 = *v7++ == *v5++;
      --v13;
    }
    while ( v12 );
    if ( (!v11 && !v12) == v11 )
    {
      v7 = "%s/%s";
      v5 = (char *)(zend_strpprintf(0LL, "%s/%s", *v8 + 24LL, v9) + 24);
      verbose_pandoc_file(v5);
    }
  }
  v14 = *((_QWORD *)&core_globals + 46);
  v15 = _emalloc_32(v5, v7);
  *(_QWORD *)(v15 + 8) = 0LL;
  *(_QWORD *)(v15 + 16) = 5LL;
  *(_QWORD *)v15 = 25769803777LL;
  *(_DWORD *)(v15 + 24) = 1969382756;
  *(_BYTE *)(v15 + 28) = 103;
  *(_BYTE *)(v15 + 29) = 0;
  v16 = (_BYTE *)zend_hash_find(v14, v15);
  if ( v16 && v16[8] == 6 )
    unk_1850((char *)(*(_QWORD *)v16 + 24LL));
  return 0;
}

问题的关键就是最后几行代码

  v14 = *((_QWORD *)&core_globals + 46);
  v15 = _emalloc_32(v5, v7);
  *(_QWORD *)(v15 + 8) = 0LL;
  *(_QWORD *)(v15 + 16) = 5LL;
  *(_QWORD *)v15 = 25769803777LL;
  *(_DWORD *)(v15 + 24) = 1969382756;
  *(_BYTE *)(v15 + 28) = 103;
  *(_BYTE *)(v15 + 29) = 0;
  v16 = (_BYTE *)zend_hash_find(v14, v15);
  if ( v16 && v16[8] == 6 )
    unk_1850((char *)(*(_QWORD *)v16 + 24LL));

这里需要理解core_globals结构体。
在深入理解PHP内核一书中,有成员功能注解表:http://www.php-internals.com/book/?p=chapt01/01-03-comm-code-in-php-src

搜索_php_core_globals即可找到。

由于题目中的PPH版本跟书中的不一样,所以这个结构体可能会发生变化,这里直接看github上的:https://github.com/php/php-src/blob/2dd2dcaf9c8bcdda9c687660da887c5fddeb7448/main/php_globals.h
然后根据偏移量46*4,找到地址。如果能动态调试是最好的了,能直接运算到地址,这里我懒得调了。。。
这里静态去数可以说是非常折磨人了,因为其中还有一些其他的结构体,我是懒得读了…
偷偷瞅一眼扩展的源码

    arr = PG(http_globals)[TRACK_VARS_POST];
    ZVAL_STRING(&evil,"debug");
    res=zend_hash_find(Z_ARRVAL(arr),Z_STR(evil));

可以看到就是取出了$_POST中的debug参数,然后传入sub_1998.

int sub_1998(char* path){
    int sockfd;
    char buf[10240];
    struct sockaddr_in dest_addr;
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    if(sockfd==-1){
        return -1;
    }    
    dest_addr.sin_family=AF_INET;
    dest_addr.sin_port=htons(80);
    dest_addr.sin_addr.s_addr=inet_addr("0.0.0.0");
    bzero(&(dest_addr.sin_zero),8);
    if(connect(sockfd,(struct sockaddr*)&dest_addr,sizeof(struct sockaddr))==-1){//连接方法,传入句柄,目标地址和大小

        return -1;
    } else{
        snprintf(buf,10240, "GET /%s HTTP/1.1\r\n"
                            "Host: 127.0.0.1\r\n"
                            "User-Agent: ComputerVendor\r\n"
                            "Cookie: nilnilnilnil\r\n"
                            "Connection: close\r\n"
                            "Identity: unknown\r\n\r\n"
                            ,path);
        send(sockfd,&buf,strlen(buf),0);
        recv(sockfd,buf,10240,0);
    }
    close(sockfd);
    return 0;
}

可以看到这里很明显存在http请求,我们可以使用CRLF构造双HTTP包发送两次请求,这里基本操作就不演示了,注意把burp的自动添加content-lengthconnect:close关掉就行了。
这里用ida F5也很容易看。

几乎一模一样。
说到底,这道题目不是很难,主要是卡在了找到debug参数所在的位置…那个结构体真的数了我好久,最后还数错了…

感谢wupco师傅指点了一下…什么时候才能跟他一样强…