Articles in the category of 安全研究

Web,安全研究,Java

JavaAgent

Javassist基础

简介

可以理解为一个加强版的反射库

不仅可以反射获取各种类 方法 参数,还可以动态修改

原理是通过修改java字节码来实现的

API

Javassist为我们提供了类似于Java反射机制的API,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

Untitled

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

Untitled

准备动手

同样 maven项目 quickstart

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.26.0-GA</version>
</dependency>
<dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.6</version>
</dependency>

我们来实现一个User类,debug方法是为了测试方便,假设真实情况不存在debug方法

package org.example;

import java.util.Random;
import org.apache.commons.lang.RandomStringUtils;
public class User {

    private String secret;
    public String flag;

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
        this.flag = "flag{0xevoA}";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }
}

假设这个类被加载到了内存中,类文件被删除,并且我们不知道flag的值,通过反射直接获取flag,如何通过javassist 修改获取flag呢?

  1. 直接将字节码写入成.class文件然后反编译
@Test
    public void jassist1() throws NotFoundException, IOException, CannotCompileException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass user = classPool.getCtClass("User");
        user.defrost();
        byte[] bytecodes = user.toBytecode();
        FileOutputStream fileOutputStream = new FileOutputStream(new File(System.getProperty("user.dir") + "/src/test/User.class"));
        fileOutputStream.write(bytecodes);
        fileOutputStream.close();
    }

不用多说了8,看看代码就行了,动手敲一下

  1. 直接输出flag变量
@Test
public void jassist2() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    ClassPool classPool = ClassPool.getDefault();
    CtClass cUser = classPool.getCtClass("User");
    cUser.defrost();
    // create method
    CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
    cUser.addMethod(ctMethod);
    Object user = cUser.toClass().newInstance();
    Method cheatEngine = user.getClass().getMethod("cheatEngine");
    cheatEngine.invoke(user);

}

新建了一个cheatEngine方法,直接输出this.flag

看代码就行,不说废话

  1. 如果flag是 private static
private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
    }

static,private在这里没什么影响sout(this.flag)照样输出,当然,如果是static的话sout(flag)也可以

  1. 修改getFlag方法,在前面加入一行代码修改input

@Test
    public void jassist3() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        {
            ClassPool classPool = ClassPool.getDefault();
            CtClass cUser = classPool.getCtClass("User");
            cUser.defrost();

            CtMethod getFlag = cUser.getDeclaredMethod("getFlag");
            System.out.println(getFlag);
            getFlag.insertBefore("$0.secret=\"123456\";");
            System.out.println();
            Object o = cUser.toClass().newInstance();
            Method getFlag1 = o.getClass().getMethod("getFlag", String.class);
            getFlag1.invoke(o,"123456");
        }

$0 就是 this

具体看上面那个表

参考https://y4er.com/post/javassist-learn/

javaAgent

introduce

javaAgent, 可以理解为java自带的hook模式,有点类似php的so拓展,以及动态连接的LD_PROLOAD

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

我们可以使用JavaAgent实现一些Hook操作

为了生成jar包方便,下面用vscode

public class User {

    private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = "secret";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }

    public static void main(String[] args) {
        User user = new User();
        if(args[0].equals("1")){
            user.debug();
        }
        if(args[0].equals("2")){
            user.getFlag(args[1]);
        }
    }
}

javac User.java 生成User.class

premain

public static void premain(String agentArgs, Instrumentation inst)

premain有两个参数,第一个agentArgs没什么用,主要关注第二个,Instrumentation

是与JVM交互的类,可以动态修改JVM加载的类字节码,结合javassist,我们可以hook所有JVM加载过的类并且为所欲为

声明

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}

Instrumentation主要关注三个方法addTransformer getAllLoadedClasses retransformClasses

  1. addTransformer

增加一个transformer(类似php-parse的NodeTraverser)

  1. getAllLoadedClasses

获取所有已经load的类

  1. retransformClasses

新建一个自定义transformer类 Agent.class

输出所有加载过的类

Agent.class

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        Class[] cs = inst.getAllLoadedClasses(); 
        for(Class c: cs){
            System.out.println(c.getName());
        }
    }
}

新建一个agent.mf

Manifest-Version: 1.0
Premain-Class: Agent
Agent-Class: Agent

运行

javac .\Agent.java 
jar cvfm agent.jar .\Agent.mf .\Agent.class
java -javaagent:agent.jar

即可输出所有load类

然后我们把javassit 和 javaagent连起来,实现

java maven项目(因为要引入javassit)

目录结构

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       ├── Agent.java
│   │   │       └── AgentTransformer.java
│   │   └── resources
│   │       └── META-INF
│   │           └── MANIFEST.MF
│   └── test
│       └── java
│           ├── com
│           │   └── AppTest.java

Agent.java

package com;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{

        ClassFileTransformer transformer = new AgentTransformer();
        inst.addTransformer(transformer);

    }
}

AgentTransformer.java

package com;
import java.lang.instrument.ClassFileTransformer;
import javassist.*;

import java.lang.reflect.Method;
import java.security.ProtectionDomain;

public class AgentTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer){

        className = className.replace("/", ".");
        if(className.equals("User")){
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass cUser = classPool.get("User");
                cUser.defrost();
                CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
                cUser.addMethod(ctMethod);
                Object user = cUser.toClass().newInstance();
                Method cheatEngine = user.getClass().getMethod("cheatEngine");
                cheatEngine.invoke(user);
            } catch (Exception e) {
                //TODO: handle exception
            }
        }
        return new byte[0];
    }

}
Manifest-Version: 1.0
Premain-Class: com.Agent

打包成jar

选择Project Structure -> Artifacts -> JAR -> From modules with dependencies

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c5a3b50-9e62-1.png

默认的配置就行。

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c81b400-9e62-1.png

选择Build -> Build Artifacts -> Build

https://xzfile.aliyuncs.com/media/upload/picture/20210416111900-7cb3a460-9e62-1.png

之后产生out/artifacts/agent_jar/agent.jar

└── out
    └── artifacts
        └── agent_jar
            └── agent.jar

然后运行User.class

java -javaagent:.\artifacts\javaagent_jar\javaagent.jar User 2 password

成功打印出flag,虽然后续报错,但目的已经达到

参考

http://wjlshare.com/archives/1582
https://xz.aliyun.com/t/9450#toc-12
https://zhishihezi.net/

- Read More -
安全研究,开源安全

M3U8解密流程

Dplayer播放器的m3u8解密脚本

https://github.com/DIYgod/DPlayer

前言

暂时只适合未加密的m3u8文件,加密的以后再写

看看能不能用go写一个 感觉会很棒,把下面都集成一下,顺便练练开发(画饼

  1. 自行下载m3u8文件(看F12 network找
  2. 通过下面脚本获取所有流文件
# pip install aiohttp
# python3.6 以上 支持asyncio
# 在脚本目录下建一个m3u8文件夹
# 默认支持bmp后缀和ts后缀 如是其他后缀下面get_all_url函数第三行bmp或ts改一下
# 最下面将https换成了http 加快下载速度 如果服务端只支持https请删除replace
# 异步比较快 但可能会报错,对已经下载的文件会跳过,报错了退出重新运行,多运行几次到全部下完
import aiohttp
import asyncio
import re
import os
import hashlib

#你下载下来的m3u8文件
filename = "video.m3u8"

#防止文件重名(我就遇到了)
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def get_all_url():
    file_content = open(filename).read()
    urls = re.findall(r"http.*?\.(bmp|ts)",file_content,re.S)
    return urls

async def fetch(client,url):
    async with client.get(url) as resp:
        return await resp.read()

async def main(url):
    if os.path.exists(f"./m3u8/{md5(url)}.ts"):
        print("url exist: break")
        return
    async with aiohttp.ClientSession() as client:
        body = await fetch(client,url)
        open("./m3u8/"+md5(url)+'.ts',"wb").write(body)
        print(f"download success: {url}")

loop = asyncio.get_event_loop()
tasks = []

for url in get_all_url():
        # !!!!!!自行观察服务器知否支持http,如果不确定就把下面.replace删了,!!!!
    task = loop.create_task(main(url.replace("https","http")))
    tasks.append(task)

#可能报错,报错了就重新运行,直到不下载新的文件
loop.run_until_complete(asyncio.wait(tasks))

Untitled

  1. 合并

网上说ffmpeg可以合并 我命令一直报错(淦

ffmpeg -allowed_extensions ALL -i hls-720p.m3u8 -c copy new.mp4

哦对我这个脚本因为可能文件名会重复md5了一下,需要把m3u8得内容换成md5文件名重新写入才行

脚本如下

同样记得看情况改下面的 bmp

import re
import os
import hashlib

filename = "video.m3u8"
new_filename = "new.m3u8"
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

file_content = open(filename).read()

with open(new_filename,"wb") as txt:
    for i in file_content.split("\n"):
        print(i)
        if re.search("http.*?\.(bmp|ts)",i,re.S):
            i = md5(i)+".ts"
            txt.write(i.encode("utf-8")+b"\n")
        else:
            txt.write(i.encode("utf-8")+b"\n")

然后合并所有文件,我直接用的python合并 3.1G的视频我本地跑了快20分钟,如果有开发大佬可以优化一下

注意下面 url=url.replace("https","http")

import hashlib
import re

filname = "video.m3u8"
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def get_all_url():
    file_content = open(filename).read()
    urls = re.findall(r"http.*?\.(bmp|ts)",file_content,re.S)
    return urls

urls = get_all_url()
filenames = []
for url in urls:
#!!!!!!!!!!!!!!!!看情况删~之前md5存的时候有没有https!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    url=url.replace("https","http")
    filenames.append(md5(url)+".ts")

big_buffer = b""

i = 1
for filename in filenames:
    print(f"now is {str(i)}| all is {str(num)} ")
    i+=1
    big_buffer += open("m3u8/"+filename,"rb").read()

open("./m3u8_lab/new.ts","wb").write(big_buffer)

Untitled

bingo

后缀直接改成mp4也可以播放,不知道是容错还是啥(来个misc选手

对了 file命令不好使file啥都是data(- -

如果后缀是ts,windows自带的播放器可以直接打开(我是win11

如果不是,改成ts或者mp4说不定能直接打开,反正file命令是真不行

- Read More -
Web,安全研究

Preface


抽象语法树(AST),即Abstract Syntax Tree的缩写。它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是抽象的,是因为这里的语法并不会表示出真实语法中出现的每个细节。


Python内置一个ast库,其中存在一些内置方法生成,遍历,获取,解析 Python代码并获取ast树,但ast库的官方文档过于简陋,基本上无法当作学习文档。


最简单的Demo


import ast
import astpretty
res = ast.parse("a = 1+1")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=BinOp(
                lineno=1,
                col_offset=4,
                end_lineno=1,
                end_col_offset=7,
                left=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=5, value=1, kind=None),
                op=Add(),
                right=Constant(lineno=1, col_offset=6, end_lineno=1, end_col_offset=7, value=1, kind=None),
            ),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0


astpretty是一个第三方库,用来美化输出ast树


上面的输出结果就是,a = 1 + 1 这句语句对应的ast树的可读形式,
其中Module(body(是所有语句dump出来都会有的,不用管
由于a = 1 + 1是一句赋值语句,所以第一层为 Assign语法
对于每一种结构语法,都会自己对应的语法属性,比如这里的Assign,就存在targets(赋值对象),value(被赋的值),而这些语法属性自己又包含了自己的属性,一层一层嵌套






需要注意的是对于某些表达式,python的ast的遍历顺序是从后到前,从外到内,这个与php的ast遍历顺序不同,有点反人类
比如

request.arg.get(123)
func(a[:5])


第一个获取到的ast对象是 get(123)
第二个获取到的也是函数 func()


当然除了打印ast树,我们也可以直接输出ast树对应的节点
1.png
假设有一道CTF题目

#secret.py
def func():
    flag = "xxxxxxxxxxxxxxxxxxxxxxxxx"
    # guess flag


假如我们可以运行一段py脚本,但是不允许读文件,执行系统命令,我们该如何获取flag?

#user.py
import secret


最简单的办法是py2的co_const
image.png
co_const属性,可以获得一个函数定义块中被定义的常量值
ast增加节点
那如果是Py3 或者 secret.py中的flag不以常量方式赋值怎么办呢
拿下面的secret.py举个例子,假设我们需要想办法获取flag的值

#secret.py
import uuid
def check():
  # Example
  flag = f"flag{{{uuid.uuid4()}}}"


现在我们允许读取文件,但是我们无法获取到uuid的值


我们可以修改ast树,在赋值的下面加一句print的ast树,然后调用ast的eval运行此函数


首先查看一个pring(flag)的ast节点


import ast
import astpretty
res = ast.parse("print(flag)")
astpretty.pprint(res)
Module(
    body=[
        Expr(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=11,
            value=Call(
                lineno=1,
                col_offset=0,
                end_lineno=1,
                end_col_offset=11,
                func=Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=5, id='print', ctx=Load()),
                args=[Name(lineno=1, col_offset=6, end_lineno=1, end_col_offset=10, id='flag', ctx=Load())],
                keywords=[],
            ),
        ),
    ],
    type_ignores=[],
)




我们模仿这个AST树,用python代码生成对应的ast结构


import ast
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)

然后把我们自己生成的节点,插入到check函数中


content = open("secret.py").read()
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)




由于print(flag)与赋值语句在同一层
而res.body[1]是函数定义结构,res.body[1].body即为函数体内,所以在此处插入res.body[1].body[0]是赋值语句,所以为insert(1,node)


ast.fix_missing_locations函数作用是自动修复 ast结构的偏移 行号等杂项,需要调用,否则会报
TypeError: required field "lineno" missing from stmt


最后只需要编译运行我们的ast树即可
exec(compile(res, '', 'exec'))


由于我们运行的语句相当于重新定义了一个check函数,所以还需要运行一遍check函数


整个文件结构如下


import ast
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.insert(1,node)
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()

ast修改节点

除了可以在下一行新增节点以外,我们还可以直接修改赋值节点,直接改为表达式节点将值输出


跟上面差不多,就不多介绍了


import ast
content = open("secret.py").read()
res = ast.parse(content)
value = res.body[1].body[0].value
func = ast.Name(id="print", ctx=ast.Load())
args = [value]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res.body[1].body[0] = node
ast.fix_missing_locations(res)
exec(compile(res, '', 'exec'))
check()




res.body[1].body[0].value 就是赋值表达式所赋的值

ast删除节点


body是一个list,把对应节点pop就行了

#secret.py
import uuid
def check():
  flag = f"flag{{{uuid.uuid4()}}}"
  flag = "88888"
  print(flag)
import ast
import astpretty
content = open("secret.py").read()
func = ast.Name(id="print", ctx=ast.Load())
args = [ast.Name(id="flag", ctx=ast.Load())]
call = ast.Call(func=func, args=args, keywords=[])
node = ast.Expr(value=call)
res = ast.parse(content)
res.body[1].body.pop(1)
ast.fix_missing_locations(res)
astpretty.pprint(res)
exec(compile(res, '', 'exec'))
check()

遍历节点

以上内容只是对某单一节点进行修改,如果我们要修改所有某一特征节点时如何去做。
ast库提供了 visit方法,供开发者遍历所有节点


import ast
content = open("secret.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeVisitor):
def visit_Assign(self,node):
  print(node.value)
  return node
m = MyVisit()
m.visit(res)
<_ast.JoinedStr object at 0x02F2F6B8>
<_ast.Constant object at 0x02F2F928>


开发者需要定义一个ast.NodeVisitor的子类,然后定义对应的visit_结构类型方法,处理特定的结构。
然后创建这个类对象,调用visit方法。遍历AST树


但此类只能进行ast的输出,如果需要一边遍历一边修改ast,增删改等,就需要继承另一个子类ast.NodeTransformer
假如我们把所有常量数字改为字符串


我们先看看字符串和数字的ast结构


import ast
import astpretty
res = ast.parse("a = 123\nv='123'")
astpretty.pprint(res)
Module(
    body=[
        Assign(
            lineno=1,
            col_offset=0,
            end_lineno=1,
            end_col_offset=7,
            targets=[Name(lineno=1, col_offset=0, end_lineno=1, end_col_offset=1, id='a', ctx=Store())],
            value=Constant(lineno=1, col_offset=4, end_lineno=1, end_col_offset=7, value=123, kind=None),
            type_comment=None,
        ),
        Assign(
            lineno=2,
            col_offset=0,
            end_lineno=2,
            end_col_offset=7,
            targets=[Name(lineno=2, col_offset=0, end_lineno=2, end_col_offset=1, id='v', ctx=Store())],
            value=Constant(lineno=2, col_offset=2, end_lineno=2, end_col_offset=7, value='123', kind=None),
            type_comment=None,
        ),
    ],
    type_ignores=[],
)

Process finished with exit code 0




很明显,他们都属于Constant结构,完全对应里面的Value属性
需要注意的是,在老的python版本里,这些常量值分别属于
ast.Num, ast.Str而不是ast.Constant


所以这个数字转字符串的遍历类如下

#case.py
a = 123
b = "231445"
def s():
  c= 3124235213
#exp.py
import ast
import astunparse
content = open("case.py").read()
res = ast.parse(content)
class MyVisit(ast.NodeTransformer):
def visit_Constant(self,node):
  if type(node.value) == int:
    node.value = str(node.value)
  return node
m = MyVisit()
node = m.visit(res)
print(astunparse.unparse(node))

a = '123'
b = '231445'
def s():
  c = '3124235213'
Process finished with exit code 0




astunparse是一个三方库,将ast树转化为python code


而如果需要删除ast节点就更简单了,在方法里return None即可

Introductionas

https://stackoverflow.com/questions/46388130/insert-a-node-into-an-abstract-syntax-tree
https://www.escapelife.site/posts/40a2fc93.html

- Read More -
安全研究

前言

如今大量前端开发皆采用现代化前端框架,如Vue和React,这类框架对于XSS提供了自带的防御措施,大部分情况下基本完全杜绝了XSS,但是在某些特定的场合下,依旧存在XSS风险,为了避免在一些渗透测试,安全服务中在这类框架上花无用的挖洞时间,此文来探讨一下此类框架是如何防御XSS以及如何产生XSS的
介于此,此文将尽可能少的介绍Vue和React的基础知识,不了解的读者可以视情况先学习一下前置知识在看此文

Vue

防御

我们知道,Vue采用数据绑定的方式,将变量绑定到dom树上,如下所示

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: 'Hello Vue.js!'
        }
    })
</script>

el代表dom的查询表达式,查询id=app的元素
然后将 {{}}里的message变量渲染为Hello Vue.js! 字符串
几乎所有的开发都这么写,那么假如我们message变量可控会如何

<div id="app">
    <p>{{ message }}</p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>

这里message被我们硬编码为了html代码,当然正常代码会动态给message赋值,这里效果一样,不额外举例,现在我们看看渲染的结果
图片.png
可以看到,即使我们可控这些变量,开发者也没有做任何过滤,我们的xss payload依旧被转义为实体字符。
这就是vue底层做的事,vue对于变量绑定,底层其实是用了Javascript原生的innerText方法,而innerText默认会把所有html元素进行转义,由于innerText也是Js原生方法,当然正常情况不可能绕过

所以有时候,我们在某些系统里狂插payload,大家会发现没有一个弹窗,可能这不是开发者防御意识高,只是人家用了框架,攻击的方式错了

隐患1

如果开发者有将html代码作为值传入到变量中的需求呢,比如动态生成表格,对于一个大型框架来说,肯定有这种渲染标签的功能,
对于Vue来说,这个功能的指令叫V-html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Vue</title>
    <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
</head>
<body>
<div id="app">
    <p v-html="message"></p>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            message: '<svg/onload=alert(1)>'
        }
    })
</script>
</body>
</html>

图片.png
很明显感觉到,v-html指令应该就是调用的document.innerHTML方法
如果开发者用v-html方法进行参数传递,并且参数可控,那么即可造成XSS

隐患2

vue用的最多的方法还是{{}},双大括号变量绑定,这个双大括号本质和 v-text指令是一样的,就是上面说的调用的innerText方法,对于一般的普通系统来说,几乎开发者全程使用{{}}进行参数传递,很少会额外用到v-html指令,那么如果不用v-html就没有常见的XSS问题了嘛?
对于一种很常见的XSS case,vue是没有做任何的防御的

<body>
<div id="app">
    <a :href="url">click me</a>
</div>

<script>
    new Vue({
        el: '#app',
        data: {
            url: "javascript:alert(111)"
        }
    })
</script>
</body>

a标签的属性xss,vue是没有任何防御措施的,我们只需要传入javascript:alert(1)即可触发XSS
除此以外
iframe的src属性,object标签的src属性都是可以触发XSS的


:href 其实是 v-bind:href的缩写
下面两个写法一样

    <a :href="url">click me</a>
    <a v-bind:href="url">click me</a>

隐患3

在javascript函数里面使用原生危险函数,比如innerHTML eval等,这个不再多说

React

React习惯用类语法来编写

隐患1

跟上诉href src一样,react不会对链接产生的xss进行过滤

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <a href={this.props.site}>click me</a>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="javascript:alert(1)"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

隐患2

同样如上v-html,react也提供了一个渲染html的方法,不过这个办法名字比较吓人
dangerouslySetInnerHTML


<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdn.staticfile.org/react/16.4.0/umd/react.development.js"></script>
    <script src="https://cdn.staticfile.org/react-dom/16.4.0/umd/react-dom.development.js"></script>
    <script src="https://cdn.staticfile.org/babel-standalone/6.26.0/babel.min.js"></script>
</head>
<body>

<div id="example"></div>
<script type="text/babel">

    class WebSite extends React.Component {
        constructor() {
            super();

        }
        render() {
            return (
                <div>
                    <p dangerouslySetInnerHTML={{__html:this.props.site}}></p>
                </div>
            );
        }
    }
    ReactDOM.render(
        <WebSite site="<svg/onload=alert(1)>"/>,
        document.getElementById('example')
    );
</script>

</body>
</html>

这个dangerouslySetInnerHTML跟vue的v-html比较类似

隐患3

同上,使用原生函数

总结

了解这些会有什么帮助

白盒审计

如果我们在对前端项目进行审核的时候,如果发现前端项目是用vue或者react进行编写的话,我们就可以只关注上面的危险函数关键字,避开其他的常规功能,比如全局搜索 如下关键字

:href
v-bind:href
:src
v-bind:src
v-html
------------------------------
href={
src={
dangerouslySetInnerHTML=

------------------------------
innerHTML
eval(
....

黑盒审计

当然,大部分时候我们是没有源代码的,我们一般面对的是一个webpack打包后的构建产物js,如果有sourcemap泄露的话,我们就可以当上面白盒进行审计,如果在没有sourcemap泄露的情况下,就必须硬怼黑盒环境了,当然黑盒会有一些技巧
一般来说,Vue和React不会单独使用,大部分与现在前端常用的UI框架结合使用,比如Vue+elementUI,React+Antdesign


这类UI框架有个很明显的特征,一般常用于CRM系统,就是什么后台管理系统这类的,像这个样子


图片.png
图片.png
图片.png
这种清一色的,左边上面菜单栏,中间内容,UI比较爽快的,大部分都是Vue+React的前端框架,所以在这里面疯狂扔<img src=#....是没有用的
正确的姿势是:
找输入链接的地方,比如网址,图片链接等输入框,插入javascript:alert(1)等,当然这里也有可能SSRF
找富文本,表格等,或者抓包发现往后端直接发送标签的地方,插入XSS poc
放弃掉其他功能,因为框架会做默认的转义

- Read More -
安全研究

前言

pty(pseudo terminal)又称伪终端,大家比较熟的可能是tty (Teletype),也就是计算机的终端设备。这篇文章就是阐述何为pty,pty本质是什么,为什么我们渗透的时拿到shell后需要获取pty,没有pty为什么处处受限。pty和tty的关系又是什么

tty

在说明pty之前,需要介绍一下tty,tty可以直接理解为终端,而介绍tty需要大概说明一下计算机的历史,tty全称为Teletypes(电传打字机),是通过串行线用打印机键盘通过阅读和发送信息的东西,后来这东西被键盘和显示器取代,所以现在叫终端比较合适。
下面是早期计算机通过电传打字机交互的模型

UART 驱动
如上图所示,物理终端通过电缆连接到计算机上的 UART(通用异步接收器和发射器)。操作系统中有一个 UART 驱动程序用于管理字节的物理传输。
线规
上图中内核中的 Line discipline(行规范)用来提供一个编辑缓冲区和一些基本的编辑命令(退格,清除单个单词,清除行,重新打印),主要用来支持用户在输入时的行为(比如输错了,需要退格)。
TTY 驱动
TTY 驱动用来进行会话管理,并且处理各种终端设备。

UART 驱动、行规范和 TTY 驱动都位于内核中,它们的一端是终端设备,另一端是用户进程。因为在 Linux 下所有的设备都是文件,所以它们三个加在一起被称为 "TTY 设备",即我们常说的 TTY。

再来看一个linux控制台的模型

虽然这个模型看上去没什么问题,但随着linux的发展,终端固定再内核层过于僵化,某些进程需要自主实现一个终端模拟器,比如ssh,xterm。而tty完全由内核接管。用户态无法使用tty的功能,于是linux提出将终端仿真移动至用户态,这就是pty的由来

当创建一个伪终端时,会在 /dev/pts 目录下创建一个设备文件:

如果是通过 PuTTY 等终端仿真程序通过 SSH 的方式远程连接 Linux,那么终端仿真程序通过 SSH 与 PTY master side 交换数据。

线规

线规(line discipline),线规是终端(tty)子系统的一部分。线规将底层设备驱动程序代码与高层通用接口例程(比如read,write等系统调用)粘合在一起,并负责实现与设备关联的语义。
例如,标准线规会根据类Unix系统上终端的要求,处理从硬件驱动程序和写入设备的应用程序接收到的数据。在输入时,它处理特殊字符,例如中断字符(通常为Control-C)以及擦除和杀死字符(通常分别为backspacedelete和Control-U),并且在输出时,它将所有LF字符替换为CR / LF序列。
通俗来讲,线规会把用户输入的某些特殊字符替换成真正用户想表达的语义,比如退格键代表删除一个字符。而不是输入一个退格键的ascii码进去。
所以,为什么我们在渗透的时候弹回来的shell,如果直接输入退格键会出现乱码,就是因为退格键没有经过线规的处理,被直接当做了一个字符。
PS:线规处于内核层

pty

如上所说,为了使应用程序能有效使用终端功能,操作系统提供了伪终端功能。那pty的实现是怎么样的呢
pty由master和slave两端构成,在任何一端的输入都会传达到另一端。与tty不同,系统中并不存在pty这种文件,它是由pts(pseudo-terminal slave)和ptmx(pseudo-teiminal master)两种设备文件来实现的。

pts

(pseudo-terminal slave)即伪终端的slave端。在Linux的/dev/pts/文件夹下有对应设设备文件。
我们可以通过tty命令查看当前用户的登录终端,如下图所示:

ubuntu@VM-32-73-ubuntu:/dev$ tty
/dev/pts/1

当我们设备文件/dev/pts/1进行输出时,屏幕上会显示相应输出:

ubuntu@VM-32-73-ubuntu:/dev$ echo hello >/dev/pts/1
hello

倘若访问别的slave文件,如/dev/pts/2,则会返回权限不足错误:(root例外)

ubuntu@VM-32-73-ubuntu:/dev$ echo hello >/dev/pts/2
-bash: /dev/pts/2: Permission denied

所以,如果我们拥有root权限,我们理论上可以控制任何伪终端的输出

ptmx

(pseudo-terminal master)
ptmx是伪终端的master端。在/dev下仅有2个ptmx文件,其信息如下:

ubuntu@VM-32-73-ubuntu:/dev$ ll /dev/ptmx
crw-rw-rw- 1 root tty 5, 2 Jan 16 16:38 /dev/ptmx
ubuntu@VM-32-73-ubuntu:/dev$ ll /dev/pts/ptmx
c--------- 1 root root 5, 2 Mar 17  2018 /dev/pts/ptmx

讲讲现象背后的故事
当ubuntu系统创建一个新的terminal时(比如上面的pts/1)
首先执行ptm = open('/dev/ptmx',...)操作
接下来fork(),然后child进程将打开'/dev/pts/1',dup2到0,1和2句柄上,随后执行execl启动一个shell.
pts = open('/dev/pts/1',...);
dup2(pts, 0); // 对应lib库中stdin
dup2(pts, 1); // 对应lib库中stdout
dup2(pts, 2); // 对应lib库中stderr
close(pts);
execl("/system/bin/sh", "/system/bin/sh", NULL);
// 这样sh输入数据将全部来自pts,
// sh的输出数据也都全部输送到pts,也就直接送到了打开ptmx的新terminal中.

新terminal将启动GUI,捕获按键数据,然后写入ptm,这样pts将收到数据,进而sh将从stdin中获得数据,
于是sh将作进一步运算,将结果送给stdout或stderr,进而送给pts,于是ptm获得数据,然后terminal的GUI
将数据显示出来.

terminal捕获到key按键值 <--> ptm <--> pts/1 <--> stdin <--> shell读到数据
shell数据结果 <--> stdout <--> pts/1 <--> ptm <--> terminal显示

因为是master - slaver,所以ptm只有一个,pts可以有多个
我们用一个ssh的图来看

+----------+       +------------+
 | Keyboard |------>|            |
 +----------+       |  Terminal  |
 | Monitor  |<------|            |
 +----------+       +------------+
                          |
                          |  ssh protocol
                          |
                          ↓
                    +------------+
                    |            |
                    | ssh server |--------------------------+
                    |            |           fork           |
                    +------------+                          |
                        |   ↑                               |
                        |   |                               |
                  write |   | read                          |
                        |   |                               |
                  +-----|---|-------------------+           |
                  |     ↓   |                   |           ↓
                  |   +--------+   +-------+    |       +-------+  fork   +-------------+
                  |   |  ptmx  |<->| pts/0 |<---------->| shell |-------->| tmux client |
                  |   +--------+   +-------+    |       +-------+         +-------------+
                  |   |        |                |                               ↑
                  |   +--------+   +-------+    |       +-------+               |
                  |   |  ptmx  |<->| pts/2 |<---------->| shell |               |
                  |   +--------+   +-------+    |       +-------+               |
                  |     ↑   |  Kernel           |           ↑                   |
                  +-----|---|-------------------+           |                   |
                        |   |                               |                   |
                        |w/r|   +---------------------------+                   |
                        |   |   |            fork                               |
                        |   ↓   |                                               |
                    +-------------+                                             |
                    |             |                                             |
                    | tmux server |<--------------------------------------------+
                    |             |
                    +-------------+

需要注意的是,由于pts是slave端,所以不支持一对多,如果我们在linux中开启两个终端分别是pts1 和 pts2
如果我们再pts2中执行 cat /dev/pts/1命令,然后我们在pts1终端中输入字符,可以发现一部分字符会回显再pts1端上,另一部分的字符会会显在pts2上。我画个图就很好理解为什么了
图片.png
当我们在pts1中输入数据时,输入流从ptmx传递给pts1在传递给bash,bash会把用户输入原样返回给输出流。这时候pts1接收到bash返还给的输出,但此时有两个应用程序在等待pts1的返回。一个是ptmx,一个是pts2下的cat进程(其实应该是pts2下bash的子进程)。于是此时就发生了数据争夺。linux内核调度器根据当时情况随时都会将他们中的一个调出或者调入,因此数据就出现了一部分被送到了pts/2的cat命令,另一部分被送到了pts1的shell,

终端与伪终端的区别

至此我们可以得出这样的结论:现在所说的终端已经不是硬件终端了,而是软件仿真终端(终端模拟软件)。
关于终端和伪终端,可以简单的理解如下:

  • 真正的硬件终端基本上已经看不到了,现在所说的终端、伪终端都是软件仿真终端(即终端模拟软件)
  • 一些连接了键盘和显示器的系统中,我们可以接触到运行在内核态的软件仿真终端(tty1-tty6)
  • 通过 SSH 等方式建立的连接中使用的都是伪终端
  • 伪终端是运行在用户态的软件仿真终端

制作rootkit

上一篇文章留下来的坑https://evoa.me/index.php/archives/64/
我们试试能不能制作一个rootkit,负责记录所有pty的输入输出,这样当我们拿下一台linux主机之后。我们就可以监控所有终端的输入输出。包括其他用户ssh连上来的和在此机器上通过ssh连别的机器的所有输入输出。
但是可惜的是,我搜遍了几乎所有,都没有找到一个完美的解决方案,唯一能让我稍微满意的,就是通过strace命令监控io系统调用。
于是我写了一个很丑的脚本,勉强能完成上诉需求。
怎么实现呢,原理很简单,一般来说pty是由一个进程来控制的,那么我们只要知道这个进程的进程id(pid),那么通过strace获取这个进程的io系统调用,write(1)代表输出,read(0)代表输入(文件描述符),然后通过正则获取参数,就可以获取pty的所有系统调用了
优点:

  1. 可以获取连接到此机器的所有伪终端的输入输出。包括不限于telnet,ssh,本地终端
  2. 可以获取到连接到此机器的基础上,在通过telnet,ssh等连接到别的机器时所有的输入输出(可无限循环)
  3. 可以获取到不回显至终端的输入(比如sudo时输入的密码,mysql连接时的密码)

缺点:

  1. 必须拥有root权限,否则只能获取和当前用户同一pty的进程的输入输出
  2. 严重依赖ptrace系统调用和strace命令
  3. echo 0 > /proc/sys/kernel/yama/ptrace_scope,当然root权限可以更改此选项

由于代码过丑,存在很多bug,我暂时就不贴出来和放在github了,等有时间写个go版本的用原生系统调用实现

大概说一下我的实现细节:

  1. 主程序第一次运行时,执行ps -ef获取当前系统所有pty进程,
  2. 删除与自身pty一样的进程
  3. 然后使用多进程或多线程运行strace命令依次获取这些进程的系统调用内容。
  4. 用正则获取所有的输入和输出,筛选(这部分很细节)
  5. 主程序运行第一次ps -ef以后会轮询ps -ef,如果发现新产生的pty进程,继续3步骤
  6. 把输入输出输出到文件或终端

说起来很容易,但是很多细节很麻烦

  1. 进程中还会有子进程,子进程还有子进程,会出现子进程退出主进程没退出或者主进程退出子进程还没从全局列表删去这些问题
  2. strace可以自动追踪子进程,但是可能和主程序的轮询冲突。
  3. trace附加到的进程无法获取父进程的输入输出,strace先附加进程,然后这个进程再产生子进程的话,strace可以追踪到,但是如果strace附加之前这个进程就已经产生的子进程,strace附加后无法获取到。
  4. write系统调用除了输出到1文件描述符会实现回显,输入到0标准输入也有回显,2标准错误也有回显,还可以直接输出到/dev/pts/x 还可以输出到/dev/tty
  5. dup2系统调用会复制一个文件描述符,我们需要追踪这个系统调用,然后判断复制的文件描述符是否是标准输入 标准输出 标准错误。需要实现一个全局列表记录
  6. close会关闭一个文件描述符,后续可能会有open或openat系统调用打开文件描述符,可能前一秒这个文件描述符是存在的,后一秒就被关闭了,再后一秒又被重新打开了并且指向发生了改变,这些都需要进行追踪

所以。。。具体实现细节过于麻烦,这也导致了我写的很难受

后话

如果想要我的残次品脚本的也可以私我。功能确实实现了,就是一堆bug

参考:
https://www.jianshu.com/p/11c01003211b
https://en.wikipedia.org/wiki/Line_discipline
http://www.linusakesson.net/programming/tty/
https://segmentfault.com/a/1190000009082089
https://www.cnblogs.com/sparkdev/p/11460821.html
https://blog.csdn.net/zhoucheng05_13/article/details/86510469

- Read More -
This is just a placeholder img.