How does Local File Inclusion (LFI) work?



  • In the past few days, I have created my own webserver to serve as my sandbox for learning pen-testing. I saw this blog (https://outpost24.com/blog/from-local-file-inclusion-to-remote-code-execution-part-1) and wanted to attempt something similar and build it on my webserver.

    This is what I have done so far:

    My index.php page here submits the input and parses the values to my action page: verify_email.php.

    echo "
    <span class='psw'><a href='#' onclick=\"document.getElementById('id04').style.display='block'\">Subscribe To Our Newsletter?</a></span>
    
    <div id='id04' class='fpass-modal'>
    <span onclick=\"document.getElementById('id04').style.display='none';\"
    class='close-fpass' title='Close Modal'>&times;</span>
    
    <!-- Modal Content -->
    <form class='fpass-modal-content' action='php/verify_email.php' method='post'>
    <div class='container4'>
    <h1>Subscribe To Our Newsletter</h1>
    <br>
    <p>Join our subscribers list to get the latest news, updates, and special offers delivered directly in your inbox</p>
    <input type='text' placeholder='Name' name='name' id='name' onfocus=\"this.value = ''\"
        required>
    <input type='text' placeholder='Email' name='email' id='email' onfocus=\"this.value = ''\"
        required>
                    <!-- Select language box -->
                    <div class='custom-select' style='padding: 8px;'>
                        <select id='language' name='language'>
                            <option value='0'>Select language:</option>
                            <option value='english.php'>English</option>
                            <option value='german.php'>German</option>
                        </select>
                    </div>
    
    <a href='#' onclick=\"document.getElementById('id04').style.display='none';\" class='backtologin'>No, thank you</a>
    
    <div class='clearfix'>
        <button type='submit' class='fpassbtn'>Subscribe</button>
    </div>
    </div>
    </form>
    </div>";
    

    The action page then uses SendGrip to automatically send an email to the submitted email for the user to verify his/her email using the included link. (something like this:)

    if ($language == "english.php") {
                $output='<p>Dear '.$name.',</p>';
                $output.='<p>Please click on the following link to verify your email.</p>';
                $output.='<p>-------------------------------------------------------------</p>';
                $output.='<p><a href="http://10.0.0.69/php/verified.php?key='.$verify_code.'&email='.$email.'&language='.$language.'&action=verify" target="_blank">
                http://10.0.0.69/php/verified.php?key='.$verify_code.'&email='.$email.'&language='.$language.'&action=verify</a></p>';
                $output.='<p>-------------------------------------------------------------</p>';
                $output.='<p>Please be sure to copy the entire link into your browser.
                The link will expire after 1 day for security reason.</p>';
                $output.='<p>If you did not request this newsletter subscription, no action 
                is needed, your account will not be subscribed to our newsletter.</p>';
                $output.='<p>For further enquiries, please reply to example@gmail.com</p>';
                $output.='<p>Thanks,</p>';
                $output.='<p>Example Team</p>';
                $body = $output;
                $subject = "Email Verification";
            }
            if ($language == "german.php") {
                $output='<p>Lieber '.$name.',</p>';
                $output.='<p>Bitte klicken Sie auf den folgenden Link, um Ihre E-Mail zu bestätigen.</p>';
                $output.='<p>-------------------------------------------------------------</p>';
                $output.='<p><a href="http://10.0.0.69/php/verified.php?key='.$verify_code.'&email='.$email.'&language='.$language.'&action=verify" target="_blank">
                http://10.0.0.69/php/verified.php?key='.$verify_code.'&email='.$email.'&language='.$language.'&action=verify</a></p>';
    .
    .
    .
    

    (sorry, I cannot expose the full codes for this part)

    Based on this link, I included the language field which can be either english.php or german.php depending on what the user selected in the form previously. Based on this field, the browser will display a page of that language to the user.

    <?php
    include 'connect.php';
    include 'validation.php';
    
    session_start();
    echo $_GET["key"];
    echo $_GET["email"];
    echo $_GET["language"];
    echo $_GET["action"];
    
    $error="";
    
    $language = $_GET["language"];
    
    include '/var/www/webdav/php/' . $language;
    ?>
    

    My english.php page:

    <?php
    if (isset($_GET["key"]) && isset($_GET["email"]) && isset($_GET["language"]) && isset($_GET["action"])
    && ($_GET["action"]=="verify")){
      $key = $_GET["key"];
      $email = $_GET["email"];
      $curDate = date("Y-m-d H:i:s");
      $query = $pdo->prepare("SELECT * FROM `subscribers` WHERE `verify_code`=:key and `email`=:email;");
      $query->bindParam(":key",$key);
      $query->bindParam(":email",$email);
      $query->execute();
      $row =$query->rowCount();
      if ($row=="") {
        $error .= '<h2>Invalid Link</h2>
        <p>The link is invalid/expired. Either you did not copy the correct link
        from the email, or you have already used the key in which case it is 
        deactivated.</p>
        <p><a href="http://10.0.0.69/">
        Click here</a> to reset password.</p>';
      }
      else{
        $reset = $query->fetch(PDO::FETCH_ASSOC);
        $expDate = $reset['expDate'];
        echo "expDate is ".$expDate;
        if ($expDate >= $curDate){
              $error="";
              $curDate = date("Y-m-d H:i:s");
            if($error!=""){
              echo "<div class='error'>".$error."</div><br />";
            }
            else{
              $query = $pdo->prepare("UPDATE `subscribers` SET `is_verified`=1, `modified`=:modified WHERE `verify_code`=:verify_code;");
    $query->bindParam(":modified",$curDate);
              $query->bindParam(":verify_code",$_GET["key"]);
              $result=$query->execute();
              if ($result) {
                echo "UPDATE OK<br>";
              }
              else{
                echo ("UPDATE Failed<br>");
                exit();
              }
    
              echo '<div class="error"><p>Thank you for subscribing to our newsletter!</p>
              <p><a href="http://localhost/cbch/mp/index.php">
              Click here</a> to Login.</p></div><br />';
            }
        }
        else{
          $error .= "<h2>Link Expired</h2>
          <p>The link is expired. You are trying to use the expired link which 
          as valid only 24 hours (1 days after request).<br /><br /></p>";
        }
      }
      if($error!=""){
        echo "<div class='error'>".$error."</div><br />";
      }
    } // isset email key validate end
    ?>
    

    Based on this, I tried doing something like http://10.0.0.69/php/verified.php?language=../../../../var/log/apache2/access.log which should theoretically display the access.log page. However, I seem to be getting a blank page instead.

    Any help would be appreciated! (PS. Sorry for making you read through this ton of codes)



  • Most linux distros/web servers have safe file permissions on logs. You would need to be root to read the access log. Exploitation is a puzzle, but you can just superglue various behaviours together as long as they mostly do something useful and does not alter execution of the program fatally. So whatever works is a good enough way to exploit it...

    Depending on the distro you may have luck with ../../../proc/self/environ and sending some PHP in a header such as the user agent.

    Failing that you can try the following options:

    • Upload a file to the server and include it
    • Include your session file (if you can manipulate its content, ie: make your firstname or username a php payload)
    • Include a mysql table file that contains some data you can manipulate to be a PHP payload
    • Don't forget you can leak the session file location, etc, by reading php.ini

    Other options:

    • Find a better bug
    • Find another bug to combine it with, ie: a CSV export function that leaves a file on the disk which you can place a PHP payload into.
    • Leak .env or config files outside webroot and use the AWS keys to access the box using SSM (if its using aws, etc).

    Extreme options:

    • Bruteforce include a temporary file (upload uploads live in /tmp with a random filename for short time even if the PHP script doesn't handle file uploads, just POST the file to any PHP script with curl) Some time before the heat death of the universe you should get a hit.
    • Do a password reset for an admin account with higher privilege and try to extract the password reset token/url from the raw mysql table by including it.


Suggested Topics

  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2